diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 159cc46e64ce..261bf042ab6c 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -36,6 +36,7 @@ export type ConfigKeyType = | 'global.calling.maxGroupCallRingSize' | 'global.groupsv2.groupSizeHardLimit' | 'global.groupsv2.maxGroupSize' + | 'global.messageQueueTimeInSeconds' | 'global.nicknames.max' | 'global.nicknames.min' | 'global.textAttachmentLimitBytes'; diff --git a/ts/services/storage.ts b/ts/services/storage.ts index 35a8d580cb5c..955bc5688a26 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -44,6 +44,7 @@ import { storageJobQueue } from '../util/JobQueue'; import { sleep } from '../util/sleep'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { map, filter } from '../util/iterables'; +import { getMessageQueueTime } from '../util/getMessageQueueTime'; import { ourProfileKeyService } from './ourProfileKey'; import { ConversationTypes, @@ -350,7 +351,10 @@ async function generateManifest( if ( storyDistributionList.deletedAtTimestamp != null && - isOlderThan(storyDistributionList.deletedAtTimestamp, durations.MONTH) + isOlderThan( + storyDistributionList.deletedAtTimestamp, + getMessageQueueTime() + ) ) { const droppedID = storyDistributionList.storageID; const droppedVersion = storyDistributionList.storageVersion; @@ -1316,7 +1320,7 @@ async function processManifest( 'unregistered and not in remote manifest' ); conversation.setUnregistered({ - timestamp: Date.now() - durations.MONTH, + timestamp: Date.now() - getMessageQueueTime(), fromStorageService: true, // Saving below diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 13ca18d60743..1f023b28dfd7 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -72,11 +72,9 @@ import { fromRootKeyBytes, getRoomIdFromRootKey, } from '../util/callLinksRingrtc'; -import { - CALL_LINK_DELETED_STORAGE_RECORD_TTL, - fromAdminKeyBytes, -} from '../util/callLinks'; +import { fromAdminKeyBytes } from '../util/callLinks'; import { isOlderThan } from '../util/timestamp'; +import { getMessageQueueTime } from '../util/getMessageQueueTime'; import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue'; const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID); @@ -1981,8 +1979,7 @@ export async function mergeCallLinkRecord( ? getTimestampFromLong(callLinkRecord.deletedAtTimestampMs) : null; const shouldDrop = - deletedAt != null && - isOlderThan(deletedAt, CALL_LINK_DELETED_STORAGE_RECORD_TTL); + deletedAt != null && isOlderThan(deletedAt, getMessageQueueTime()); if (shouldDrop) { details.push('expired deleted call link; scheduling for removal'); } diff --git a/ts/services/tapToViewMessagesDeletionService.ts b/ts/services/tapToViewMessagesDeletionService.ts index 6f9ef294bb12..23c05c5f2c85 100644 --- a/ts/services/tapToViewMessagesDeletionService.ts +++ b/ts/services/tapToViewMessagesDeletionService.ts @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import { DataReader } from '../sql/Client'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; -import { DAY } from '../util/durations'; +import { getMessageQueueTime } from '../util/getMessageQueueTime'; import * as Errors from '../types/errors'; async function eraseTapToViewMessages() { @@ -12,7 +12,9 @@ async function eraseTapToViewMessages() { window.SignalContext.log.info( 'eraseTapToViewMessages: Loading messages...' ); - const messages = await DataReader.getTapToViewMessagesNeedingErase(); + const maxTimestamp = Date.now() - getMessageQueueTime(); + const messages = + await DataReader.getTapToViewMessagesNeedingErase(maxTimestamp); await Promise.all( messages.map(async fromDB => { const message = window.MessageCache.__DEPRECATED$register( @@ -59,7 +61,7 @@ class TapToViewMessagesDeletionService { return; } - const nextCheck = receivedAt + 30 * DAY; + const nextCheck = receivedAt + getMessageQueueTime(); window.SignalContext.log.info( 'checkTapToViewMessages: next check at', new Date(nextCheck).toISOString() diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 8c266ac5f3e1..36c4abea0343 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -541,7 +541,9 @@ type ReadableInterface = { getMessagesUnexpectedlyMissingExpirationStartTimestamp: () => Array; getSoonestMessageExpiry: () => undefined | number; getNextTapToViewMessageTimestampToAgeOut: () => undefined | number; - getTapToViewMessagesNeedingErase: () => Array; + getTapToViewMessagesNeedingErase: ( + maxTimestamp: number + ) => Array; // getOlderMessagesByConversation is JSON on server, full message on Client getAllStories: (options: { conversationId?: string; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index fc8c81c7fa06..2db8b52d028a 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -4524,9 +4524,10 @@ function getNextTapToViewMessageTimestampToAgeOut( return isNormalNumber(result) ? result : undefined; } -function getTapToViewMessagesNeedingErase(db: ReadableDB): Array { - const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000; - +function getTapToViewMessagesNeedingErase( + db: ReadableDB, + maxTimestamp: number +): Array { const rows: JSONRows = db .prepare( ` @@ -4535,12 +4536,12 @@ function getTapToViewMessagesNeedingErase(db: ReadableDB): Array { WHERE isViewOnce = 1 AND (isErased IS NULL OR isErased != 1) - AND received_at <= $THIRTY_DAYS_AGO + AND received_at <= $maxTimestamp ORDER BY received_at ASC, sent_at ASC; ` ) .all({ - THIRTY_DAYS_AGO, + maxTimestamp, }); return rows.map(row => jsonToObject(row.json)); diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index dd17c0027adf..f05c07826206 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -59,6 +59,7 @@ import * as log from '../logging/log'; import type { StorageAccessType } from '../types/Storage'; import { getRelativePath, createName } from '../util/attachmentPath'; import { isBackupEnabled } from '../util/isBackupEnabled'; +import { getMessageQueueTime } from '../util/getMessageQueueTime'; type StorageKeyByServiceIdKind = { [kind in ServiceIdKind]: keyof StorageAccessType; @@ -77,7 +78,6 @@ export const KYBER_KEY_ID_KEY: StorageKeyByServiceIdKind = { [ServiceIdKind.PNI]: 'maxKyberPreKeyIdPNI', }; -const LAST_RESORT_KEY_ARCHIVE_AGE = 30 * DAY; const LAST_RESORT_KEY_ROTATION_AGE = DAY * 1.5; const LAST_RESORT_KEY_MINIMUM = 5; const LAST_RESORT_KEY_UPDATE_TIME_KEY: StorageKeyByServiceIdKind = { @@ -96,7 +96,6 @@ const PRE_KEY_ID_KEY: StorageKeyByServiceIdKind = { }; const PRE_KEY_MINIMUM = 10; -const SIGNED_PRE_KEY_ARCHIVE_AGE = 30 * DAY; export const SIGNED_PRE_KEY_ID_KEY: StorageKeyByServiceIdKind = { [ServiceIdKind.ACI]: 'signedKeyId', [ServiceIdKind.Unknown]: 'signedKeyId', @@ -756,7 +755,7 @@ export default class AccountManager extends EventTarget { 'confirmed' ); - // Keep SIGNED_PRE_KEY_MINIMUM keys, drop if older than SIGNED_PRE_KEY_ARCHIVE_AGE + // Keep SIGNED_PRE_KEY_MINIMUM keys, drop if older than message queue time const toDelete: Array = []; sortedKeys.forEach((key, index) => { @@ -765,7 +764,7 @@ export default class AccountManager extends EventTarget { } const createdAt = key.created_at || 0; - if (isOlderThan(createdAt, SIGNED_PRE_KEY_ARCHIVE_AGE)) { + if (isOlderThan(createdAt, getMessageQueueTime())) { const timestamp = new Date(createdAt).toJSON(); const confirmedText = key.confirmed ? ' (confirmed)' : ''; log.info( @@ -813,7 +812,7 @@ export default class AccountManager extends EventTarget { 'confirmed' ); - // Keep LAST_RESORT_KEY_MINIMUM keys, drop if older than LAST_RESORT_KEY_ARCHIVE_AGE + // Keep LAST_RESORT_KEY_MINIMUM keys, drop if older than message queue time const toDelete: Array = []; sortedKeys.forEach((key, index) => { @@ -822,7 +821,7 @@ export default class AccountManager extends EventTarget { } const createdAt = key.createdAt || 0; - if (isOlderThan(createdAt, LAST_RESORT_KEY_ARCHIVE_AGE)) { + if (isOlderThan(createdAt, getMessageQueueTime())) { const timestamp = new Date(createdAt).toJSON(); const confirmedText = key.isConfirmed ? ' (confirmed)' : ''; log.info( diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 8cbe77cd7e2b..dc6ff5cf87c2 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -30,6 +30,7 @@ import { strictAssert } from '../util/assert'; import type { SignalService as Proto } from '../protobuf'; import { isMoreRecentThan } from '../util/timestamp'; import { DAY } from '../util/durations'; +import { getMessageQueueTime } from '../util/getMessageQueueTime'; import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl'; import type { ReencryptionInfo } from '../AttachmentCrypto'; @@ -1156,10 +1157,9 @@ export function isReencryptableWithNewEncryptionInfo( ); } -const TIME_ON_TRANSIT_TIER = 30 * DAY; // Extend range in case the attachment is actually still there (this function is meant to // be optimistic) -const BUFFERED_TIME_ON_TRANSIT_TIER = TIME_ON_TRANSIT_TIER + 5 * DAY; +const BUFFER_TIME_ON_TRANSIT_TIER = 5 * DAY; export function mightStillBeOnTransitTier( attachment: Pick @@ -1177,7 +1177,10 @@ export function mightStillBeOnTransitTier( } if ( - isMoreRecentThan(attachment.uploadTimestamp, BUFFERED_TIME_ON_TRANSIT_TIER) + isMoreRecentThan( + attachment.uploadTimestamp, + getMessageQueueTime() + BUFFER_TIME_ON_TRANSIT_TIER + ) ) { return true; } diff --git a/ts/util/callLinks.ts b/ts/util/callLinks.ts index 462740691816..bc90b4026a38 100644 --- a/ts/util/callLinks.ts +++ b/ts/util/callLinks.ts @@ -15,7 +15,6 @@ import { type CallHistoryDetails, CallMode, } from '../types/CallDisposition'; -import { DAY } from './durations'; export const CALL_LINK_DEFAULT_STATE: Pick< CallLinkType, @@ -28,8 +27,6 @@ export const CALL_LINK_DEFAULT_STATE: Pick< storageNeedsSync: false, }; -export const CALL_LINK_DELETED_STORAGE_RECORD_TTL = 30 * DAY; - export function getKeyFromCallLink(callLink: string): string { const url = new URL(callLink); if (url == null) { diff --git a/ts/util/getMessageQueueTime.ts b/ts/util/getMessageQueueTime.ts new file mode 100644 index 000000000000..bb8b535faa0f --- /dev/null +++ b/ts/util/getMessageQueueTime.ts @@ -0,0 +1,18 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as RemoteConfig from '../RemoteConfig'; +import { MONTH, SECOND } from './durations'; +import { parseIntWithFallback } from './parseIntWithFallback'; + +export function getMessageQueueTime(): number { + return ( + Math.max( + parseIntWithFallback( + RemoteConfig.getValue('global.messageQueueTimeInSeconds'), + MONTH / SECOND + ), + MONTH / SECOND + ) * SECOND + ); +} diff --git a/ts/util/isConversationUnregistered.ts b/ts/util/isConversationUnregistered.ts index 4f142d18d5e9..b7789dd9d337 100644 --- a/ts/util/isConversationUnregistered.ts +++ b/ts/util/isConversationUnregistered.ts @@ -3,7 +3,8 @@ import type { ServiceIdString } from '../types/ServiceId'; import { isMoreRecentThan, isOlderThan } from './timestamp'; -import { HOUR, MONTH } from './durations'; +import { HOUR } from './durations'; +import { getMessageQueueTime } from './getMessageQueueTime'; const SIX_HOURS = 6 * HOUR; @@ -45,6 +46,7 @@ export function isConversationUnregisteredAndStale({ } return Boolean( - firstUnregisteredAt && isOlderThan(firstUnregisteredAt, MONTH) + firstUnregisteredAt && + isOlderThan(firstUnregisteredAt, getMessageQueueTime()) ); }