Sender Key: Prepare for production

This commit is contained in:
Scott Nonnenberg 2021-06-08 14:51:58 -07:00 committed by GitHub
parent f226822dff
commit bff3f0c74a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 334 additions and 183 deletions

View file

@ -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;

View file

@ -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'

View file

@ -425,30 +425,6 @@ export async function startApp(): Promise<void> {
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<void> {
// 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<void> {
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<void> {
return false;
}
async function archiveSessionOnMatch({
requesterUuid,
requesterDevice,
senderDevice,
}: RetryRequestType): Promise<void> {
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<void> {
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<void> {
if (!targetMessage) {
window.log.info(`onRetryRequest/${logId}: Did not find message`);
await sendDistributionMessageOrNullMessage(retryRequest);
return;
}
@ -3454,47 +3566,36 @@ export async function startApp(): Promise<void> {
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<void> {
);
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<void> {
// 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<void> {
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) {

View file

@ -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,

View file

@ -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
);
})();

View file

@ -3601,9 +3601,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
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,

View file

@ -805,7 +805,7 @@ export class CallingClass {
window.Signal.Util.sendToGroup(
{ groupCallUpdate: { eraId }, groupV2, timestamp },
conversation,
ContentHint.SUPPLEMENTARY,
ContentHint.DEFAULT,
sendOptions
),
timestamp,

View file

@ -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<RetryItemType> = [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<RetryItemType> = [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<RetryItemType> = [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<RetryItemType> = [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<RetryItemType> = [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<RetryItemType> = [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(),

3
ts/textsecure.d.ts vendored
View file

@ -1411,7 +1411,8 @@ export declare namespace UnidentifiedSenderMessageClass.Message {
}
class ContentHint {
static SUPPLEMENTARY: number;
static DEFAULT: number;
static RESENDABLE: number;
static IMPLICIT: number;
}
}

View file

@ -93,6 +93,7 @@ export type DecryptionErrorType = z.infer<typeof decryptionErrorTypeSchema>;
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);
}

View file

@ -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
);

View file

@ -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<typeof retryItemSchema>;
@ -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<Array<RetryItemType>> {
const expiration = getOneHourAgo();
const expiration = getDeltaIntoPast(this.retryReceiptLifespan);
const max = this.items.length;
const result: Array<RetryItemType> = [];
@ -152,28 +156,24 @@ export class RetryPlaceholders {
return result;
}
async findByConversationAndRemove(
conversationId: string
): Promise<Array<RetryItemType>> {
const result = this.byConversation[conversationId];
if (!result) {
return [];
async findByConversationAndMarkOpened(conversationId: string): Promise<void> {
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(

View file

@ -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,

View file

@ -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();