Sender Key: Prepare for production
This commit is contained in:
parent
f226822dff
commit
bff3f0c74a
14 changed files with 334 additions and 183 deletions
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
246
ts/background.ts
246
ts/background.ts
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -805,7 +805,7 @@ export class CallingClass {
|
|||
window.Signal.Util.sendToGroup(
|
||||
{ groupCallUpdate: { eraId }, groupV2, timestamp },
|
||||
conversation,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
ContentHint.DEFAULT,
|
||||
sendOptions
|
||||
),
|
||||
timestamp,
|
||||
|
|
|
@ -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
3
ts/textsecure.d.ts
vendored
|
@ -1411,7 +1411,8 @@ export declare namespace UnidentifiedSenderMessageClass.Message {
|
|||
}
|
||||
|
||||
class ContentHint {
|
||||
static SUPPLEMENTARY: number;
|
||||
static DEFAULT: number;
|
||||
static RESENDABLE: number;
|
||||
static IMPLICIT: number;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue