From bff3f0c74a54efb166b671bd5979fae16fd448bb Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 8 Jun 2021 14:51:58 -0700 Subject: [PATCH] Sender Key: Prepare for production --- protos/UnidentifiedDelivery.proto | 12 +- ts/RemoteConfig.ts | 3 + ts/background.ts | 246 ++++++++++++++------ ts/groups.ts | 6 +- ts/models/conversations.ts | 12 +- ts/models/messages.ts | 8 +- ts/services/calling.ts | 2 +- ts/test-both/util/retryPlaceholders_test.ts | 113 ++++++--- ts/textsecure.d.ts | 3 +- ts/textsecure/MessageReceiver.ts | 6 +- ts/textsecure/SendMessage.ts | 44 ++-- ts/util/retryPlaceholders.ts | 50 ++-- ts/util/sendToGroup.ts | 7 +- ts/views/conversation_view.ts | 5 +- 14 files changed, 334 insertions(+), 183 deletions(-) diff --git a/protos/UnidentifiedDelivery.proto b/protos/UnidentifiedDelivery.proto index d17f614b0..d0a6f9fc0 100644 --- a/protos/UnidentifiedDelivery.proto +++ b/protos/UnidentifiedDelivery.proto @@ -43,14 +43,14 @@ message UnidentifiedSenderMessage { } enum ContentHint { - // Our parser does not handle reserved in enums: DESKTOP-1569 - // reserved 0; // A content hint of "default" should never be encoded. + // Show an error immediately; it was important but we can't retry. + DEFAULT = 0; - // Do not insert an error. - SUPPLEMENTARY = 1; + // Sender will try to resend; delay any error UI if possible + RESENDABLE = 1; - // Put an invisible placeholder in the chat (using the groupId from the sealed sender envelope if available) and delay showing an error until later. - RESENDABLE = 2; + // Don't show any error UI at all; this is something sent implicitly like a typing message or a receipt + IMPLICIT = 2; } optional Type type = 1; diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 2af6f2b76..9d18cf179 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -11,7 +11,10 @@ export type ConfigKeyType = | 'desktop.gv2' | 'desktop.mandatoryProfileSharing' | 'desktop.messageRequests' + | 'desktop.retryReceiptLifespan' + | 'desktop.retryRespondMaxAge' | 'desktop.screensharing2' + | 'desktop.sendSenderKey' | 'desktop.storage' | 'desktop.storageWrite3' | 'desktop.worksAtSignal' diff --git a/ts/background.ts b/ts/background.ts index d25827f88..e48a0c7ae 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -425,30 +425,6 @@ export async function startApp(): Promise { first = false; cleanupSessionResets(); - const retryPlaceholders = new window.Signal.Util.RetryPlaceholders(); - window.Signal.Services.retryPlaceholders = retryPlaceholders; - - setInterval(async () => { - const expired = await retryPlaceholders.getExpiredAndRemove(); - window.log.info( - `retryPlaceholders/interval: Found ${expired.length} expired items` - ); - expired.forEach(item => { - const { conversationId, senderUuid } = item; - const conversation = window.ConversationController.get(conversationId); - if (conversation) { - const receivedAt = Date.now(); - const receivedAtCounter = window.Signal.Util.incrementMessageCounter(); - conversation.queueJob(() => - conversation.addDeliveryIssue({ - receivedAt, - receivedAtCounter, - senderUuid, - }) - ); - } - }); - }, FIVE_MINUTES); // These make key operations available to IPC handlers created in preload.js window.Events = { @@ -835,6 +811,46 @@ export async function startApp(): Promise { // we generate on load of each convo. window.Signal.RemoteConfig.initRemoteConfig(); + let retryReceiptLifespan: number | undefined; + try { + retryReceiptLifespan = parseIntOrThrow( + window.Signal.RemoteConfig.getValue('desktop.retryReceiptLifespan'), + 'retryReceiptLifeSpan' + ); + } catch (error) { + window.log.warn( + 'Failed to parse integer out of desktop.retryReceiptLifespan feature flag', + error && error.stack ? error.stack : error + ); + } + + const retryPlaceholders = new window.Signal.Util.RetryPlaceholders({ + retryReceiptLifespan, + }); + window.Signal.Services.retryPlaceholders = retryPlaceholders; + + setInterval(async () => { + const expired = await retryPlaceholders.getExpiredAndRemove(); + window.log.info( + `retryPlaceholders/interval: Found ${expired.length} expired items` + ); + expired.forEach(item => { + const { conversationId, senderUuid } = item; + const conversation = window.ConversationController.get(conversationId); + if (conversation) { + const receivedAt = Date.now(); + const receivedAtCounter = window.Signal.Util.incrementMessageCounter(); + conversation.queueJob(() => + conversation.addDeliveryIssue({ + receivedAt, + receivedAtCounter, + senderUuid, + }) + ); + } + }); + }, FIVE_MINUTES); + try { await Promise.all([ window.ConversationController.load(), @@ -2148,7 +2164,9 @@ export async function startApp(): Promise { await server.registerCapabilities({ 'gv2-3': true, 'gv1-migration': true, - senderKey: false, + senderKey: window.Signal.RemoteConfig.isEnabled( + 'desktop.sendSenderKey' + ), }); } catch (error) { window.log.error( @@ -3408,13 +3426,106 @@ export async function startApp(): Promise { return false; } + async function archiveSessionOnMatch({ + requesterUuid, + requesterDevice, + senderDevice, + }: RetryRequestType): Promise { + const ourDeviceId = parseIntOrThrow( + window.textsecure.storage.user.getDeviceId(), + 'archiveSessionOnMatch/getDeviceId' + ); + if (ourDeviceId === senderDevice) { + const address = `${requesterUuid}.${requesterDevice}`; + window.log.info( + 'archiveSessionOnMatch: Devices match, archiving session' + ); + await window.textsecure.storage.protocol.archiveSession(address); + } + } + + async function sendDistributionMessageOrNullMessage( + options: RetryRequestType + ): Promise { + const { groupId, requesterUuid } = options; + let sentDistributionMessage = false; + window.log.info('sendDistributionMessageOrNullMessage: Starting', { + groupId: groupId ? `groupv2(${groupId})` : undefined, + requesterUuid, + }); + + await archiveSessionOnMatch(options); + + const conversation = window.ConversationController.getOrCreate( + requesterUuid, + 'private' + ); + + if (groupId) { + const group = window.ConversationController.get(groupId); + const distributionId = group?.get('senderKeyInfo')?.distributionId; + + if (group && distributionId) { + window.log.info( + 'sendDistributionMessageOrNullMessage: Found matching group, sending sender key distribution message' + ); + + try { + const { + ContentHint, + } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + + const result = await window.textsecure.messaging.sendSenderKeyDistributionMessage( + { + contentHint: ContentHint.DEFAULT, + distributionId, + groupId, + identifiers: [requesterUuid], + } + ); + if (result.errors && result.errors.length > 0) { + throw result.errors[0]; + } + sentDistributionMessage = true; + } catch (error) { + window.log.error( + 'sendDistributionMessageOrNullMessage: Failed to send sender key distribution message', + error && error.stack ? error.stack : error + ); + } + } + } + + if (!sentDistributionMessage) { + window.log.info( + 'sendDistributionMessageOrNullMessage: Did not send distribution message, sending null message' + ); + + try { + const sendOptions = await conversation.getSendOptions(); + const result = await window.textsecure.messaging.sendNullMessage( + { uuid: requesterUuid }, + sendOptions + ); + if (result.errors && result.errors.length > 0) { + throw result.errors[0]; + } + } catch (error) { + window.log.error( + 'maybeSendDistributionMessage: Failed to send null message', + error && error.stack ? error.stack : error + ); + } + } + } + async function onRetryRequest(event: RetryRequestEventType) { const { retryRequest } = event; const { - requesterUuid, requesterDevice, - sentAt, + requesterUuid, senderDevice, + sentAt, } = retryRequest; const logId = `${requesterUuid}.${requesterDevice} ${sentAt}-${senderDevice}`; @@ -3447,6 +3558,7 @@ export async function startApp(): Promise { if (!targetMessage) { window.log.info(`onRetryRequest/${logId}: Did not find message`); + await sendDistributionMessageOrNullMessage(retryRequest); return; } @@ -3454,47 +3566,36 @@ export async function startApp(): Promise { window.log.info( `onRetryRequest/${logId}: Message is erased, refusing to send again.` ); + await sendDistributionMessageOrNullMessage(retryRequest); return; } const HOUR = 60 * 60 * 1000; const ONE_DAY = 24 * HOUR; - if (isOlderThan(sentAt, ONE_DAY)) { + let retryRespondMaxAge = ONE_DAY; + try { + retryRespondMaxAge = parseIntOrThrow( + window.Signal.RemoteConfig.getValue('desktop.retryRespondMaxAge'), + 'retryRespondMaxAge' + ); + } catch (error) { + window.log.warn( + `onRetryRequest/${logId}: Failed to parse integer from desktop.retryRespondMaxAge feature flag`, + error && error.stack ? error.stack : error + ); + } + + if (isOlderThan(sentAt, retryRespondMaxAge)) { window.log.info( `onRetryRequest/${logId}: Message is too old, refusing to send again.` ); + await sendDistributionMessageOrNullMessage(retryRequest); return; } - const sentUnidentified = isInList( - requesterConversation, - targetMessage.get('unidentifiedDeliveries') - ); - const wasDelivered = isInList( - requesterConversation, - targetMessage.get('delivered_to') - ); - if (sentUnidentified && wasDelivered) { - window.log.info( - `onRetryRequest/${logId}: Message was sent sealed sender and was delivered, refusing to send again.` - ); - return; - } - - const ourDeviceId = parseIntOrThrow( - window.textsecure.storage.user.getDeviceId(), - 'onRetryRequest/getDeviceId' - ); - if (ourDeviceId === senderDevice) { - const address = `${requesterUuid}.${requesterDevice}`; - window.log.info( - `onRetryRequest/${logId}: Devices match, archiving session` - ); - await window.textsecure.storage.protocol.archiveSession(address); - } - window.log.info(`onRetryRequest/${logId}: Resending message`); - targetMessage.resend(requesterUuid); + await archiveSessionOnMatch(retryRequest); + await targetMessage.resend(requesterUuid); } type DecryptionErrorEventType = Event & { @@ -3558,16 +3659,6 @@ export async function startApp(): Promise { ); const conversation = group || sender; - function immediatelyAddError() { - conversation.queueJob(async () => { - conversation.addDeliveryIssue({ - receivedAt: receivedAtDate, - receivedAtCounter, - senderUuid, - }); - }); - } - // 2. Send resend request if (!cipherTextBytes || !isNumber(cipherTextType)) { @@ -3611,14 +3702,7 @@ export async function startApp(): Promise { // 3. Determine how to represent this to the user. Three different options. - // This is a sync message of some kind that cannot be resent. Reset session but don't - // show any UI for it. - if (contentHint === ContentHint.SUPPLEMENTARY) { - scheduleSessionReset(senderUuid, senderDevice); - return; - } - - // If we request a re-send, it might just work out for us! + // We believe that it could be successfully re-sent, so we'll add a placeholder. if (contentHint === ContentHint.RESENDABLE) { const { retryPlaceholders } = window.Signal.Services; assert(retryPlaceholders, 'requestResend: adding placeholder'); @@ -3635,10 +3719,22 @@ export async function startApp(): Promise { return; } + // This message cannot be resent. We'll show no error and trust the other side to + // reset their session. + if (contentHint === ContentHint.IMPLICIT) { + return; + } + window.log.warn( `requestResend/${logId}: No content hint, adding error immediately` ); - immediatelyAddError(); + conversation.queueJob(async () => { + conversation.addDeliveryIssue({ + receivedAt: receivedAtDate, + receivedAtCounter, + senderUuid, + }); + }); } function scheduleSessionReset(senderUuid: string, senderDevice: number) { diff --git a/ts/groups.ts b/ts/groups.ts index 609a6dee8..6fc226f83 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -1322,7 +1322,7 @@ export async function modifyGroupV2({ profileKey, }, conversation, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, sendOptions ) ); @@ -1696,7 +1696,7 @@ export async function createGroupV2({ profileKey, }, conversation, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, sendOptions ), timestamp, @@ -2226,7 +2226,7 @@ export async function initiateMigrationToGroupV2( profileKey: ourProfileKey, }, conversation, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, sendOptions ), timestamp, diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index f1f537bb8..522b1ddaa 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -1201,7 +1201,7 @@ export class ConversationModel extends window.Backbone timestamp, groupMembers, contentMessage, - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, undefined, { ...sendOptions, @@ -1212,7 +1212,7 @@ export class ConversationModel extends window.Backbone } else { handleMessageSend( window.Signal.Util.sendContentMessageToGroup({ - contentHint: ContentHint.SUPPLEMENTARY, + contentHint: ContentHint.IMPLICIT, contentMessage, conversation: this, online: true, @@ -3289,7 +3289,7 @@ export class ConversationModel extends window.Backbone targetTimestamp, timestamp, undefined, // expireTimer - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, undefined, // groupId profileKey, options @@ -3305,7 +3305,7 @@ export class ConversationModel extends window.Backbone profileKey, }, this, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, options ); })(); @@ -3446,7 +3446,7 @@ export class ConversationModel extends window.Backbone undefined, // deletedForEveryoneTimestamp timestamp, expireTimer, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, undefined, // groupId profileKey, options @@ -3465,7 +3465,7 @@ export class ConversationModel extends window.Backbone profileKey, }, this, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, options ); })(); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 9a79b76d1..3f90b3e05 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -3601,9 +3601,13 @@ export class MessageModel extends window.Backbone.Model { conversationId, message.get('sent_at') ); - if (item) { + if (item && item.wasOpened) { window.log.info( - `handleDataMessage: found retry placeholder. Updating ${message.idForLogging()} received_at/received_at_ms` + `handleDataMessage: found retry placeholder for ${message.idForLogging()}, but conversation was opened. No updates made.` + ); + } else if (item) { + window.log.info( + `handleDataMessage: found retry placeholder for ${message.idForLogging()}. Updating received_at/received_at_ms` ); message.set({ received_at: item.receivedAtCounter, diff --git a/ts/services/calling.ts b/ts/services/calling.ts index bb43a708a..7657478f2 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -805,7 +805,7 @@ export class CallingClass { window.Signal.Util.sendToGroup( { groupCallUpdate: { eraId }, groupV2, timestamp }, conversation, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, sendOptions ), timestamp, diff --git a/ts/test-both/util/retryPlaceholders_test.ts b/ts/test-both/util/retryPlaceholders_test.ts index 1b7f71218..f39699225 100644 --- a/ts/test-both/util/retryPlaceholders_test.ts +++ b/ts/test-both/util/retryPlaceholders_test.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; +import sinon from 'sinon'; import { - getOneHourAgo, + getDeltaIntoPast, RetryItemType, RetryPlaceholders, STORAGE_KEY, @@ -13,15 +14,26 @@ import { /* eslint-disable @typescript-eslint/no-explicit-any */ describe('RetryPlaceholders', () => { + const NOW = 1_000_000; + let clock: any; + beforeEach(() => { window.storage.put(STORAGE_KEY, null); + + clock = sinon.useFakeTimers({ + now: NOW, + }); + }); + + afterEach(() => { + clock.restore(); }); function getDefaultItem(): RetryItemType { return { conversationId: 'conversation-id', - sentAt: Date.now() - 10, - receivedAt: Date.now() - 5, + sentAt: NOW - 10, + receivedAt: NOW - 5, receivedAtCounter: 4, senderUuid: 'sender-uuid', }; @@ -87,11 +99,11 @@ describe('RetryPlaceholders', () => { it('returns soonest expiration given a list, and after add', async () => { const older = { ...getDefaultItem(), - receivedAt: Date.now(), + receivedAt: NOW, }; const newer = { ...getDefaultItem(), - receivedAt: Date.now() + 10, + receivedAt: NOW + 10, }; const items: Array = [older, newer]; window.storage.put(STORAGE_KEY, items); @@ -102,7 +114,7 @@ describe('RetryPlaceholders', () => { const oldest = { ...getDefaultItem(), - receivedAt: Date.now() - 5, + receivedAt: NOW - 5, }; await placeholders.add(oldest); @@ -115,11 +127,11 @@ describe('RetryPlaceholders', () => { it('does nothing if no item expired', async () => { const older = { ...getDefaultItem(), - receivedAt: Date.now() + 10, + receivedAt: NOW + 10, }; const newer = { ...getDefaultItem(), - receivedAt: Date.now() + 15, + receivedAt: NOW + 15, }; const items: Array = [older, newer]; window.storage.put(STORAGE_KEY, items); @@ -132,11 +144,11 @@ describe('RetryPlaceholders', () => { it('removes just one if expired', async () => { const older = { ...getDefaultItem(), - receivedAt: getOneHourAgo() - 1000, + receivedAt: getDeltaIntoPast() - 1000, }; const newer = { ...getDefaultItem(), - receivedAt: Date.now() + 15, + receivedAt: NOW + 15, }; const items: Array = [older, newer]; window.storage.put(STORAGE_KEY, items); @@ -150,11 +162,11 @@ describe('RetryPlaceholders', () => { it('removes all if expired', async () => { const older = { ...getDefaultItem(), - receivedAt: getOneHourAgo() - 1000, + receivedAt: getDeltaIntoPast() - 1000, }; const newer = { ...getDefaultItem(), - receivedAt: getOneHourAgo() - 900, + receivedAt: getDeltaIntoPast() - 900, }; const items: Array = [older, newer]; window.storage.put(STORAGE_KEY, items); @@ -169,7 +181,7 @@ describe('RetryPlaceholders', () => { }); }); - describe('#findByConversationAndRemove', () => { + describe('#findByConversationAndMarkOpened', () => { it('does nothing if no items found matching conversation', async () => { const older = { ...getDefaultItem(), @@ -184,68 +196,101 @@ describe('RetryPlaceholders', () => { const placeholders = new RetryPlaceholders(); assert.strictEqual(2, placeholders.getCount()); - assert.deepEqual( - [], - await placeholders.findByConversationAndRemove('conversation-id-3') - ); + await placeholders.findByConversationAndMarkOpened('conversation-id-3'); assert.strictEqual(2, placeholders.getCount()); + + const saveItems = window.storage.get(STORAGE_KEY); + assert.deepEqual([older, newer], saveItems); }); - it('removes all items matching conversation', async () => { + it('updates all items matching conversation', async () => { const convo1a = { ...getDefaultItem(), conversationId: 'conversation-id-1', - receivedAt: Date.now() - 5, + receivedAt: NOW - 5, }; const convo1b = { ...getDefaultItem(), conversationId: 'conversation-id-1', - receivedAt: Date.now() - 4, + receivedAt: NOW - 4, }; const convo2a = { ...getDefaultItem(), conversationId: 'conversation-id-2', - receivedAt: Date.now() + 15, + receivedAt: NOW + 15, }; const items: Array = [convo1a, convo1b, convo2a]; window.storage.put(STORAGE_KEY, items); const placeholders = new RetryPlaceholders(); assert.strictEqual(3, placeholders.getCount()); + await placeholders.findByConversationAndMarkOpened('conversation-id-1'); + assert.strictEqual(3, placeholders.getCount()); + + const firstSaveItems = window.storage.get(STORAGE_KEY); assert.deepEqual( - [convo1a, convo1b], - await placeholders.findByConversationAndRemove('conversation-id-1') + [ + { + ...convo1a, + wasOpened: true, + }, + { + ...convo1b, + wasOpened: true, + }, + convo2a, + ], + firstSaveItems ); - assert.strictEqual(1, placeholders.getCount()); const convo2b = { ...getDefaultItem(), conversationId: 'conversation-id-2', - receivedAt: Date.now() + 16, + receivedAt: NOW + 16, }; await placeholders.add(convo2b); - assert.strictEqual(2, placeholders.getCount()); + assert.strictEqual(4, placeholders.getCount()); + await placeholders.findByConversationAndMarkOpened('conversation-id-2'); + assert.strictEqual(4, placeholders.getCount()); + + const secondSaveItems = window.storage.get(STORAGE_KEY); assert.deepEqual( - [convo2a, convo2b], - await placeholders.findByConversationAndRemove('conversation-id-2') + [ + { + ...convo1a, + wasOpened: true, + }, + { + ...convo1b, + wasOpened: true, + }, + { + ...convo2a, + wasOpened: true, + }, + { + ...convo2b, + wasOpened: true, + }, + ], + secondSaveItems ); - assert.strictEqual(0, placeholders.getCount()); }); }); describe('#findByMessageAndRemove', () => { it('does nothing if no item matching message found', async () => { - const sentAt = Date.now() - 20; + const sentAt = NOW - 20; const older = { ...getDefaultItem(), conversationId: 'conversation-id-1', - sentAt: Date.now() - 10, + sentAt: NOW - 10, }; const newer = { ...getDefaultItem(), conversationId: 'conversation-id-1', - sentAt: Date.now() - 11, + sentAt: NOW - 11, }; const items: Array = [older, newer]; window.storage.put(STORAGE_KEY, items); @@ -258,12 +303,12 @@ describe('RetryPlaceholders', () => { assert.strictEqual(2, placeholders.getCount()); }); it('removes the item matching message', async () => { - const sentAt = Date.now() - 20; + const sentAt = NOW - 20; const older = { ...getDefaultItem(), conversationId: 'conversation-id-1', - sentAt: Date.now() - 10, + sentAt: NOW - 10, }; const newer = { ...getDefaultItem(), diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index 0bc8c6657..f7b2bd871 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -1411,7 +1411,8 @@ export declare namespace UnidentifiedSenderMessageClass.Message { } class ContentHint { - static SUPPLEMENTARY: number; + static DEFAULT: number; static RESENDABLE: number; + static IMPLICIT: number; } } diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index a18c38d2b..5e3a0e787 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -93,6 +93,7 @@ export type DecryptionErrorType = z.infer; const retryRequestTypeSchema = z .object({ + groupId: z.string().optional(), requesterUuid: z.string(), requesterDevice: z.number(), senderDevice: z.number(), @@ -1757,10 +1758,11 @@ class MessageReceiverInner extends EventTarget { const event = new Event('retry-request'); event.retryRequest = { - sentAt: request.timestamp(), - requesterUuid: sourceUuid, + groupId: envelope.groupId, requesterDevice: sourceDevice, + requesterUuid: sourceUuid, senderDevice: request.deviceId(), + sentAt: request.timestamp(), }; await this.dispatchAndWait(event); } diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index c01b06d63..55821850b 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -1039,7 +1039,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, timestamp, - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1067,7 +1067,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1098,7 +1098,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1128,7 +1128,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1160,7 +1160,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1196,7 +1196,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1228,7 +1228,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1266,7 +1266,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, options ); } @@ -1306,7 +1306,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1347,7 +1347,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, sendOptions ); } @@ -1395,7 +1395,7 @@ export default class MessageSender { myUuid || myNumber, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1451,7 +1451,7 @@ export default class MessageSender { myUuid || myNumber, secondMessage, now, - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); }); @@ -1484,7 +1484,7 @@ export default class MessageSender { } : {}), }, - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, undefined, // groupId sendOptions ); @@ -1509,7 +1509,7 @@ export default class MessageSender { finalTimestamp, recipients, contentMessage, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, undefined, // groupId sendOptions ); @@ -1547,7 +1547,7 @@ export default class MessageSender { recipientUuid || recipientE164, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1573,7 +1573,7 @@ export default class MessageSender { senderUuid || senderE164, contentMessage, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1608,7 +1608,7 @@ export default class MessageSender { identifier, contentMessage, timestamp, - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, options ); } @@ -1649,7 +1649,7 @@ export default class MessageSender { identifier, proto, timestamp, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, options ).catch(logError('resetSession/sendToContact error:')); }) @@ -1702,7 +1702,7 @@ export default class MessageSender { flags: window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, }, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, undefined, // groupId options ); @@ -1725,7 +1725,7 @@ export default class MessageSender { Date.now(), [uuid], plaintext, - ContentHint.SUPPLEMENTARY, + ContentHint.IMPLICIT, undefined, // groupId options ); @@ -1864,7 +1864,7 @@ export default class MessageSender { groupIdentifiers, proto, Date.now(), - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, undefined, // only for GV2 ids options ); @@ -1911,7 +1911,7 @@ export default class MessageSender { } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; return this.sendMessage( attrs, - ContentHint.SUPPLEMENTARY, + ContentHint.DEFAULT, undefined, // only for GV2 ids options ); diff --git a/ts/util/retryPlaceholders.ts b/ts/util/retryPlaceholders.ts index 5f8b3f364..a37d4274f 100644 --- a/ts/util/retryPlaceholders.ts +++ b/ts/util/retryPlaceholders.ts @@ -11,6 +11,7 @@ const retryItemSchema = z receivedAt: z.number(), receivedAtCounter: z.number(), senderUuid: z.string(), + wasOpened: z.boolean().optional(), }) .passthrough(); export type RetryItemType = z.infer; @@ -30,8 +31,8 @@ export function getItemId(conversationId: string, sentAt: number): string { const HOUR = 60 * 60 * 1000; export const STORAGE_KEY = 'retryPlaceholders'; -export function getOneHourAgo(): number { - return Date.now() - HOUR; +export function getDeltaIntoPast(delta?: number): number { + return Date.now() - (delta || HOUR); } export class RetryPlaceholders { @@ -41,7 +42,9 @@ export class RetryPlaceholders { private byMessage: ByMessageLookupType; - constructor() { + private retryReceiptLifespan: number; + + constructor(options: { retryReceiptLifespan?: number } = {}) { if (!window.storage) { throw new Error( 'RetryPlaceholders.constructor: window.storage not available!' @@ -67,6 +70,7 @@ export class RetryPlaceholders { this.sortByExpiresAtAsc(); this.byConversation = this.makeByConversationLookup(); this.byMessage = this.makeByMessageLookup(); + this.retryReceiptLifespan = options.retryReceiptLifespan || HOUR; } // Arranging local data for efficiency @@ -128,7 +132,7 @@ export class RetryPlaceholders { } async getExpiredAndRemove(): Promise> { - const expiration = getOneHourAgo(); + const expiration = getDeltaIntoPast(this.retryReceiptLifespan); const max = this.items.length; const result: Array = []; @@ -152,28 +156,24 @@ export class RetryPlaceholders { return result; } - async findByConversationAndRemove( - conversationId: string - ): Promise> { - const result = this.byConversation[conversationId]; - if (!result) { - return []; + async findByConversationAndMarkOpened(conversationId: string): Promise { + let changed = 0; + const items = this.byConversation[conversationId]; + (items || []).forEach(item => { + if (!item.wasOpened) { + changed += 1; + // eslint-disable-next-line no-param-reassign + item.wasOpened = true; + } + }); + + if (changed > 0) { + window.log.info( + `RetryPlaceholders.findByConversationAndMarkOpened: Updated ${changed} items for conversation ${conversationId}` + ); + + await this.save(); } - - const items = this.items.filter( - item => item.conversationId !== conversationId - ); - - window.log.info( - `RetryPlaceholders.findByConversationAndRemove: Found ${result.length} expired items` - ); - - this.items = items; - this.sortByExpiresAtAsc(); - this.makeLookups(); - await this.save(); - - return result; } async findByMessageAndRemove( diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index 40bf22b05..c8e0eb3cf 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -16,6 +16,7 @@ import { padMessage, SenderCertificateMode, } from '../textsecure/OutgoingMessage'; +import { isEnabled } from '../RemoteConfig'; import { isOlderThan } from './timestamp'; import { @@ -116,6 +117,7 @@ export async function sendContentMessageToGroup({ const ourConversation = window.ConversationController.get(ourConversationId); if ( + isEnabled('desktop.sendSenderKey') && ourConversation?.get('capabilities')?.senderKey && isGroupV2(conversation.attributes) ) { @@ -199,8 +201,9 @@ export async function sendToGroupViaSenderKey(options: { } if ( + contentHint !== ContentHint.DEFAULT && contentHint !== ContentHint.RESENDABLE && - contentHint !== ContentHint.SUPPLEMENTARY + contentHint !== ContentHint.IMPLICIT ) { throw new Error( `sendToGroupViaSenderKey/${logId}: Invalid contentHint ${contentHint}` @@ -326,7 +329,7 @@ export async function sendToGroupViaSenderKey(options: { ); await window.textsecure.messaging.sendSenderKeyDistributionMessage( { - contentHint: ContentHint.SUPPLEMENTARY, + contentHint: ContentHint.DEFAULT, distributionId, groupId, identifiers: newToMemberUuids, diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 9d3536428..fa3bc49b4 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -2266,10 +2266,7 @@ Whisper.ConversationView = Whisper.View.extend({ const { retryPlaceholders } = window.Signal.Services; if (retryPlaceholders) { - const placeholders = await retryPlaceholders.findByConversationAndRemove( - model.id - ); - window.log.info(`onOpened: Found ${placeholders.length} placeholders`); + await retryPlaceholders.findByConversationAndMarkOpened(model.id); } this.loadNewestMessages();