Support for message retry requests
This commit is contained in:
parent
28f016ce48
commit
ee513a1965
37 changed files with 1996 additions and 359 deletions
|
@ -1117,6 +1117,38 @@
|
|||
"message": "Contact Support",
|
||||
"description": "Shown on explainer dialog available from chat session refreshed timeline events"
|
||||
},
|
||||
"DeliveryIssue--preview": {
|
||||
"message": "Delivery issue",
|
||||
"description": "Shown in left pane preview when message delivery issue happens"
|
||||
},
|
||||
"DeliveryIssue--notification": {
|
||||
"message": "A message from $sender$ couldn’t be delivered",
|
||||
"description": "Shown in timeline when message delivery issue happens",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "Alice"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeliveryIssue--learnMore": {
|
||||
"message": "Learn More",
|
||||
"description": "Shown in timeline when message delivery issue happens, to provide access to a popup info dialog"
|
||||
},
|
||||
"DeliveryIssue--title": {
|
||||
"message": "Delivery Issue",
|
||||
"description": "Shown on explainer dialog available from delivery issue timeline events"
|
||||
},
|
||||
"DeliveryIssue--summary": {
|
||||
"message": "A message, sticker, reaction, read receipt or media couldn’t be delivered to you from $sender$. They may have tried sending it to you directly, or in a group.",
|
||||
"description": "Shown on explainer dialog available from delivery issue timeline events",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "Alice"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quoteThumbnailAlt": {
|
||||
"message": "Thumbnail of image from quoted message",
|
||||
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"
|
||||
|
|
13
images/delivery-issue.svg
Normal file
13
images/delivery-issue.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg width="200" height="110" viewBox="0 0 200 110" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M39.2999 15.1L12.8999 15C9.0999 15 5.9999 18.1 5.9999 21.9L5.8999 88C5.8999 91.8 8.9999 94.9 12.7999 94.9L39.0999 95C42.8999 95 45.9999 91.9 45.9999 88.1L46.1999 22C46.1999 18.2 43.0999 15.1 39.2999 15.1ZM42.7999 88.1C42.7999 90.1 41.0999 91.8 39.0999 91.8L12.6999 91.7C10.6999 91.7 8.9999 90 8.9999 88L9.0999 21.9C9.0999 19.9 10.7999 18.2 12.7999 18.2L39.1999 18.3C41.1999 18.3 42.8999 20 42.8999 22L42.7999 88.1Z" fill="#C6C6C6"/>
|
||||
<path d="M187.3 15.1L160.9 15C157.1 15 154 18.1 154 21.9L153.9 88C153.9 91.8 157 94.9 160.8 94.9L187.2 95C191 95 194.1 91.9 194.1 88.1L194.2 22C194.2 18.2 191.1 15.1 187.3 15.1ZM190.8 88.1C190.8 90.1 189.1 91.8 187.1 91.8L160.7 91.7C158.7 91.7 157 90 157 88L157.1 21.9C157.1 19.9 158.8 18.2 160.8 18.2L187.2 18.3C189.2 18.3 190.9 20 190.9 22L190.8 88.1Z" fill="#C6C6C6"/>
|
||||
<path d="M126 58C127.105 58 128 57.1046 128 56C128 54.8954 127.105 54 126 54C124.895 54 124 54.8954 124 56C124 57.1046 124.895 58 126 58Z" fill="#848484"/>
|
||||
<path d="M136 58C137.105 58 138 57.1046 138 56C138 54.8954 137.105 54 136 54C134.895 54 134 54.8954 134 56C134 57.1046 134.895 58 136 58Z" fill="#848484"/>
|
||||
<path d="M146 58C147.105 58 148 57.1046 148 56C148 54.8954 147.105 54 146 54C144.895 54 144 54.8954 144 56C144 57.1046 144.895 58 146 58Z" fill="#848484"/>
|
||||
<path d="M54 58C55.1046 58 56 57.1046 56 56C56 54.8954 55.1046 54 54 54C52.8954 54 52 54.8954 52 56C52 57.1046 52.8954 58 54 58Z" fill="#848484"/>
|
||||
<path d="M64 58C65.1046 58 66 57.1046 66 56C66 54.8954 65.1046 54 64 54C62.8954 54 62 54.8954 62 56C62 57.1046 62.8954 58 64 58Z" fill="#848484"/>
|
||||
<path d="M74 58C75.1046 58 76 57.1046 76 56C76 54.8954 75.1046 54 74 54C72.8954 54 72 54.8954 72 56C72 57.1046 72.8954 58 74 58Z" fill="#848484"/>
|
||||
<path d="M118 56C118 65.9411 109.941 74 100 74C90.0589 74 82 65.9411 82 56C82 46.0589 90.0589 38 100 38C109.941 38 118 46.0589 118 56Z" fill="#FFC207"/>
|
||||
<path d="M102.083 47H97.9167L98.75 58.25H101.25L102.083 47Z" fill="white"/>
|
||||
<path d="M102.079 61.5C102.353 61.87 102.5 62.305 102.5 62.75C102.5 63.3467 102.237 63.919 101.768 64.341C101.299 64.7629 100.663 65 100 65C99.5055 65 99.0222 64.868 98.6111 64.6208C98.2 64.3736 97.8795 64.0222 97.6903 63.611C97.5011 63.1999 97.4516 62.7475 97.548 62.311C97.6445 61.8746 97.8826 61.4737 98.2322 61.159C98.5819 60.8443 99.0273 60.63 99.5123 60.5432C99.9972 60.4564 100.5 60.501 100.957 60.6713C101.414 60.8416 101.804 61.13 102.079 61.5Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -44,7 +44,7 @@ describe('MessageReceiver', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('generates light-session-reset event when it cannot decrypt', done => {
|
||||
it('generates decryption-error event when it cannot decrypt', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081/');
|
||||
|
||||
mockServer.on('connection', server => {
|
||||
|
@ -63,82 +63,33 @@ describe('MessageReceiver', () => {
|
|||
}
|
||||
);
|
||||
|
||||
messageReceiver.addEventListener('light-session-reset', done());
|
||||
messageReceiver.addEventListener('decrytion-error', done());
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
let messageReceiver;
|
||||
let mockServer;
|
||||
// For when we start testing individual MessageReceiver methods
|
||||
|
||||
beforeEach(() => {
|
||||
// Necessary to populate the server property inside of MockSocket. Without it, we
|
||||
// crash when doing any number of things to a MockSocket instance.
|
||||
mockServer = new MockServer('ws://localhost:8081');
|
||||
// describe('methods', () => {
|
||||
// let messageReceiver;
|
||||
// let mockServer;
|
||||
|
||||
messageReceiver = new textsecure.MessageReceiver(
|
||||
'oldUsername.3',
|
||||
'username.3',
|
||||
'password',
|
||||
'signalingKey',
|
||||
{
|
||||
serverTrustRoot: 'AAAAAAAA',
|
||||
}
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockServer.close();
|
||||
});
|
||||
// beforeEach(() => {
|
||||
// // Necessary to populate the server property inside of MockSocket. Without it, we
|
||||
// // crash when doing any number of things to a MockSocket instance.
|
||||
// mockServer = new MockServer('ws://localhost:8081');
|
||||
|
||||
describe('#isOverHourIntoPast', () => {
|
||||
it('returns false for now', () => {
|
||||
assert.isFalse(messageReceiver.isOverHourIntoPast(Date.now()));
|
||||
});
|
||||
it('returns false for 5 minutes ago', () => {
|
||||
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
||||
assert.isFalse(messageReceiver.isOverHourIntoPast(fiveMinutesAgo));
|
||||
});
|
||||
it('returns true for 65 minutes ago', () => {
|
||||
const sixtyFiveMinutesAgo = Date.now() - 65 * 60 * 1000;
|
||||
assert.isTrue(messageReceiver.isOverHourIntoPast(sixtyFiveMinutesAgo));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#cleanupSessionResets', () => {
|
||||
it('leaves empty object alone', () => {
|
||||
window.storage.put('sessionResets', {});
|
||||
messageReceiver.cleanupSessionResets();
|
||||
const actual = window.storage.get('sessionResets');
|
||||
|
||||
const expected = {};
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
it('filters out any timestamp older than one hour', () => {
|
||||
const startValue = {
|
||||
one: Date.now() - 1,
|
||||
two: Date.now(),
|
||||
three: Date.now() - 65 * 60 * 1000,
|
||||
};
|
||||
window.storage.put('sessionResets', startValue);
|
||||
messageReceiver.cleanupSessionResets();
|
||||
const actual = window.storage.get('sessionResets');
|
||||
|
||||
const expected = window._.pick(startValue, ['one', 'two']);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
it('filters out falsey items', () => {
|
||||
const startValue = {
|
||||
one: 0,
|
||||
two: false,
|
||||
three: Date.now(),
|
||||
};
|
||||
window.storage.put('sessionResets', startValue);
|
||||
messageReceiver.cleanupSessionResets();
|
||||
const actual = window.storage.get('sessionResets');
|
||||
|
||||
const expected = window._.pick(startValue, ['three']);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
// messageReceiver = new textsecure.MessageReceiver(
|
||||
// 'oldUsername.3',
|
||||
// 'username.3',
|
||||
// 'password',
|
||||
// 'signalingKey',
|
||||
// {
|
||||
// serverTrustRoot: 'AAAAAAAA',
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// afterEach(() => {
|
||||
// mockServer.close();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
"fs-xattr": "0.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@signalapp/signal-client": "0.6.0",
|
||||
"@signalapp/signal-client": "0.8.0",
|
||||
"@sindresorhus/is": "0.8.0",
|
||||
"@types/pino": "6.3.6",
|
||||
"@types/pino-multi-stream": "5.1.0",
|
||||
|
|
|
@ -12,6 +12,11 @@ message Envelope {
|
|||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNIDENTIFIED_SENDER = 6;
|
||||
|
||||
// Our parser does not handle reserved in enums: DESKTOP-1569
|
||||
// reserved 7;
|
||||
|
||||
PLAINTEXT_CONTENT = 8;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
|
@ -34,6 +39,7 @@ message Content {
|
|||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
optional bytes senderKeyDistributionMessage = 7;
|
||||
optional bytes decryptionErrorMessage = 8;
|
||||
}
|
||||
|
||||
// Everything in CallingMessage must be kept in sync with RingRTC (ringrtc-node).
|
||||
|
|
|
@ -39,16 +39,18 @@ message UnidentifiedSenderMessage {
|
|||
// reserved 3 to 6;
|
||||
|
||||
SENDERKEY_MESSAGE = 7;
|
||||
PLAINTEXT_CONTENT = 8;
|
||||
}
|
||||
|
||||
enum ContentHint {
|
||||
// Commented out here, even though it is correct syntax. Our parser cannot handle it.
|
||||
|
||||
// Our parser does not handle reserved in enums: DESKTOP-1569
|
||||
// reserved 0; // A content hint of "default" should never be encoded.
|
||||
|
||||
// Do not insert an error.
|
||||
SUPPLEMENTARY = 1;
|
||||
RETRY = 2;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
|
|
|
@ -10385,16 +10385,62 @@ $contact-modal-padding: 18px;
|
|||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
// Module: Chat Session Refreshed Dialog
|
||||
// Module: Delivery Issue Notification
|
||||
|
||||
.module-delivery-issue-notification {
|
||||
@include font-body-2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.module-delivery-issue-notification__first-line {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.module-chat-session-refreshed-dialog {
|
||||
width: 360px;
|
||||
padding: 16px;
|
||||
padding-top: 28px;
|
||||
border-radius: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
.module-delivery-issue-notification__icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/error-outline-12.svg',
|
||||
$color-gray-60
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/error-outline-12.svg',
|
||||
$color-gray-25
|
||||
);
|
||||
}
|
||||
}
|
||||
.module-delivery-issue-notification__button {
|
||||
@include button-reset;
|
||||
@include button-light-blue-text;
|
||||
@include button-small;
|
||||
|
||||
@include font-body-2;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
// Module: Chat Session Refreshed Dialog
|
||||
|
||||
.module-chat-session-refreshed-dialog {
|
||||
@include light-theme {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
@ -10413,6 +10459,7 @@ $contact-modal-padding: 18px;
|
|||
.module-chat-session-refreshed-dialog__buttons {
|
||||
text-align: right;
|
||||
margin-top: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
.module-chat-session-refreshed-dialog__button {
|
||||
@include font-body-1-bold;
|
||||
|
@ -10427,6 +10474,42 @@ $contact-modal-padding: 18px;
|
|||
@include button-secondary;
|
||||
}
|
||||
|
||||
// Module: Delivery Issue Dialog
|
||||
|
||||
.module-delivery-issue-dialog {
|
||||
// margin-left: auto;
|
||||
// margin-right: auto;
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-white;
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-95;
|
||||
}
|
||||
}
|
||||
.module-delivery-issue-dialog__image {
|
||||
text-align: center;
|
||||
}
|
||||
.module-delivery-issue-dialog__title {
|
||||
@include font-body-1-bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.module-delivery-issue-dialog__buttons {
|
||||
text-align: right;
|
||||
margin-top: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
.module-delivery-issue-dialog__button {
|
||||
@include font-body-1-bold;
|
||||
@include button-reset;
|
||||
@include button-primary;
|
||||
|
||||
border-radius: 4px;
|
||||
padding: 7px 14px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* Third-party module: react-contextmenu*/
|
||||
|
||||
.react-contextmenu {
|
||||
|
|
|
@ -3,12 +3,19 @@
|
|||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
|
||||
const ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js');
|
||||
const Long = require('../components/long/dist/Long.js');
|
||||
const { setEnvironment, Environment } = require('../ts/environment');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
setEnvironment(Environment.Test);
|
||||
|
||||
const storageMap = new Map();
|
||||
|
||||
// To replicate logic we have on the client side
|
||||
global.window = {
|
||||
log: {
|
||||
|
@ -21,6 +28,10 @@ global.window = {
|
|||
ByteBuffer,
|
||||
Long,
|
||||
},
|
||||
storage: {
|
||||
get: key => storageMap.get(key),
|
||||
put: async (key, value) => storageMap.set(key, value),
|
||||
},
|
||||
};
|
||||
|
||||
// For ducks/network.getEmptyState()
|
||||
|
|
333
ts/background.ts
333
ts/background.ts
|
@ -1,6 +1,12 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
import {
|
||||
DecryptionErrorMessage,
|
||||
PlaintextContent,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { DataMessageClass } from './textsecure.d';
|
||||
import { MessageAttributesType } from './model-types.d';
|
||||
import { WhatIsThis } from './window.d';
|
||||
|
@ -22,10 +28,38 @@ import { ourProfileKeyService } from './services/ourProfileKey';
|
|||
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
||||
import { setToExpire } from './services/MessageUpdater';
|
||||
import { LatestQueue } from './util/LatestQueue';
|
||||
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||
import {
|
||||
DecryptionErrorType,
|
||||
RetryRequestType,
|
||||
} from './textsecure/MessageReceiver';
|
||||
import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
}
|
||||
|
||||
type SessionResetsType = Record<string, number>;
|
||||
export async function cleanupSessionResets(): Promise<void> {
|
||||
const sessionResets = window.storage.get<SessionResetsType>(
|
||||
'sessionResets',
|
||||
{}
|
||||
);
|
||||
|
||||
const keys = Object.keys(sessionResets);
|
||||
keys.forEach(key => {
|
||||
const timestamp = sessionResets[key];
|
||||
if (!timestamp || isOverHourIntoPast(timestamp)) {
|
||||
delete sessionResets[key];
|
||||
}
|
||||
});
|
||||
|
||||
await window.storage.put('sessionResets', sessionResets);
|
||||
}
|
||||
|
||||
export async function startApp(): Promise<void> {
|
||||
window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
|
||||
window.attachmentDownloadQueue = [];
|
||||
|
@ -377,6 +411,27 @@ 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 now = Date.now();
|
||||
conversation.queueJob(() =>
|
||||
conversation.addDeliveryIssue(now, senderUuid)
|
||||
);
|
||||
}
|
||||
});
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
// These make key operations available to IPC handlers created in preload.js
|
||||
window.Events = {
|
||||
getDeviceName: () => window.textsecure.storage.user.getDeviceName(),
|
||||
|
@ -1949,7 +2004,8 @@ export async function startApp(): Promise<void> {
|
|||
addQueuedEventListener('read', onReadReceipt);
|
||||
addQueuedEventListener('verified', onVerified);
|
||||
addQueuedEventListener('error', onError);
|
||||
addQueuedEventListener('light-session-reset', onLightSessionReset);
|
||||
addQueuedEventListener('decryption-error', onDecryptionError);
|
||||
addQueuedEventListener('retry-request', onRetryRequest);
|
||||
addQueuedEventListener('empty', onEmpty);
|
||||
addQueuedEventListener('reconnect', onReconnect);
|
||||
addQueuedEventListener('configuration', onConfiguration);
|
||||
|
@ -2061,7 +2117,7 @@ export async function startApp(): Promise<void> {
|
|||
await server.registerCapabilities({
|
||||
'gv2-3': true,
|
||||
'gv1-migration': true,
|
||||
senderKey: false,
|
||||
senderKey: true,
|
||||
});
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
|
@ -3287,18 +3343,271 @@ export async function startApp(): Promise<void> {
|
|||
window.log.warn('background onError: Doing nothing with incoming error');
|
||||
}
|
||||
|
||||
type LightSessionResetEventType = Event & {
|
||||
senderUuid: string;
|
||||
senderDevice: number;
|
||||
type RetryRequestEventType = Event & {
|
||||
retryRequest: RetryRequestType;
|
||||
};
|
||||
|
||||
function onLightSessionReset(event: LightSessionResetEventType) {
|
||||
const { senderUuid, senderDevice } = event;
|
||||
function isInList(
|
||||
conversation: ConversationModel,
|
||||
list: Array<string | undefined | null> | undefined
|
||||
): boolean {
|
||||
const uuid = conversation.get('uuid');
|
||||
const e164 = conversation.get('e164');
|
||||
const id = conversation.get('id');
|
||||
|
||||
if (event.confirm) {
|
||||
event.confirm();
|
||||
if (!list) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (list.includes(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uuid && list.includes(uuid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e164 && list.includes(e164)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function onRetryRequest(event: RetryRequestEventType) {
|
||||
const { retryRequest } = event;
|
||||
const {
|
||||
requesterUuid,
|
||||
requesterDevice,
|
||||
sentAt,
|
||||
senderDevice,
|
||||
} = retryRequest;
|
||||
|
||||
window.log.info('onRetryRequest:', {
|
||||
requesterUuid,
|
||||
requesterDevice,
|
||||
sentAt,
|
||||
senderDevice,
|
||||
});
|
||||
|
||||
const requesterConversation = window.ConversationController.getOrCreate(
|
||||
requesterUuid,
|
||||
'private'
|
||||
);
|
||||
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(sentAt, {
|
||||
MessageCollection: window.Whisper.MessageCollection,
|
||||
});
|
||||
|
||||
const targetMessage = messages.find(message => {
|
||||
if (message.get('sent_at') !== sentAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.get('type') !== 'outgoing') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isInList(requesterConversation, message.get('sent_to'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!targetMessage) {
|
||||
window.log.info(
|
||||
`onRetryRequest: Did not find message sent at ${sentAt}, sent to ${requesterUuid}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetMessage.isErased()) {
|
||||
window.log.info(
|
||||
`onRetryRequest: Message sent at ${sentAt} is erased, refusing to send again.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
const ONE_DAY = 24 * HOUR;
|
||||
if (isOlderThan(sentAt, ONE_DAY)) {
|
||||
window.log.info(
|
||||
`onRetryRequest: Message sent at ${sentAt} is too old, refusing to send again.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sentUnidentified = isInList(
|
||||
requesterConversation,
|
||||
targetMessage.get('unidentifiedDeliveries')
|
||||
);
|
||||
const wasDelivered = isInList(
|
||||
requesterConversation,
|
||||
targetMessage.get('delivered_to')
|
||||
);
|
||||
if (sentUnidentified && wasDelivered) {
|
||||
window.log.info(
|
||||
`onRetryRequest: Message sent at ${sentAt} was sent sealed sender and was delivered, refusing to send again.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
`onRetryRequest: Resending message ${sentAt} to user ${requesterUuid}`
|
||||
);
|
||||
|
||||
const ourDeviceId = parseIntOrThrow(
|
||||
window.textsecure.storage.user.getDeviceId(),
|
||||
'onRetryRequest/getDeviceId'
|
||||
);
|
||||
if (ourDeviceId === senderDevice) {
|
||||
const address = `${requesterUuid}.${requesterDevice}`;
|
||||
window.log.info(
|
||||
`onRetryRequest: Devices match, archiving session with ${address}`
|
||||
);
|
||||
await window.textsecure.storage.protocol.archiveSession(address);
|
||||
}
|
||||
|
||||
targetMessage.resend(requesterUuid);
|
||||
}
|
||||
|
||||
type DecryptionErrorEventType = Event & {
|
||||
decryptionError: DecryptionErrorType;
|
||||
};
|
||||
|
||||
async function onDecryptionError(event: DecryptionErrorEventType) {
|
||||
const { decryptionError } = event;
|
||||
const { senderUuid, senderDevice } = decryptionError;
|
||||
|
||||
window.log.info(`onDecryptionError: ${senderUuid}.${senderDevice}`);
|
||||
|
||||
const conversation = window.ConversationController.getOrCreate(
|
||||
senderUuid,
|
||||
'private'
|
||||
);
|
||||
const capabilities = conversation.get('capabilities');
|
||||
if (!capabilities) {
|
||||
await conversation.getProfiles();
|
||||
}
|
||||
|
||||
if (conversation.get('capabilities')?.senderKey) {
|
||||
requestResend(decryptionError);
|
||||
return;
|
||||
}
|
||||
|
||||
await startAutomaticSessionReset(decryptionError);
|
||||
}
|
||||
|
||||
async function requestResend(decryptionError: DecryptionErrorType) {
|
||||
const {
|
||||
cipherTextBytes,
|
||||
cipherTextType,
|
||||
contentHint,
|
||||
groupId,
|
||||
receivedAtCounter,
|
||||
receivedAtDate,
|
||||
senderDevice,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
} = decryptionError;
|
||||
|
||||
window.log.info(`requestResend: ${senderUuid}.${senderDevice}`, {
|
||||
cipherTextBytesLength: cipherTextBytes?.byteLength,
|
||||
cipherTextType,
|
||||
contentHint,
|
||||
groupId: groupId ? `groupv2(${groupId})` : undefined,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
// 1. Find the target conversation
|
||||
|
||||
const group = groupId
|
||||
? window.ConversationController.get(groupId)
|
||||
: undefined;
|
||||
const sender = window.ConversationController.getOrCreate(
|
||||
senderUuid,
|
||||
'private'
|
||||
);
|
||||
const conversation = group || sender;
|
||||
|
||||
function immediatelyAddError() {
|
||||
const receivedAt = Date.now();
|
||||
conversation.queueJob(async () => {
|
||||
conversation.addDeliveryIssue(receivedAt, senderUuid);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Send resend request
|
||||
|
||||
if (!cipherTextBytes || !isNumber(cipherTextType)) {
|
||||
window.log.warn(
|
||||
'requestResend: Missing cipherText information, failing over to automatic reset'
|
||||
);
|
||||
startAutomaticSessionReset(decryptionError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = DecryptionErrorMessage.forOriginal(
|
||||
Buffer.from(cipherTextBytes),
|
||||
cipherTextType,
|
||||
timestamp,
|
||||
senderDevice
|
||||
);
|
||||
|
||||
const plaintext = PlaintextContent.from(message);
|
||||
const options = await conversation.getSendOptions();
|
||||
const result = await window.textsecure.messaging.sendRetryRequest({
|
||||
plaintext,
|
||||
options,
|
||||
uuid: senderUuid,
|
||||
});
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
throw result.errors[0];
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'requestResend: Failed to send retry request, failing over to automatic reset',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
startAutomaticSessionReset(decryptionError);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
// 3. Determine how to represent this to the user. Three different options.
|
||||
|
||||
// This is a sync message of some kind that cannot be resent. Don't do anything.
|
||||
if (contentHint === ContentHint.SUPPLEMENTARY) {
|
||||
scheduleSessionReset(senderUuid, senderDevice);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we request a re-send, it might just work out for us!
|
||||
if (contentHint === ContentHint.RESENDABLE) {
|
||||
const { retryPlaceholders } = window.Signal.Services;
|
||||
assert(retryPlaceholders, 'requestResend: adding placeholder');
|
||||
|
||||
window.log.warn('requestResend: Adding placeholder');
|
||||
await retryPlaceholders.add({
|
||||
conversationId: conversation.get('id'),
|
||||
receivedAt: receivedAtDate,
|
||||
receivedAtCounter,
|
||||
sentAt: timestamp,
|
||||
senderUuid,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
immediatelyAddError();
|
||||
}
|
||||
|
||||
function scheduleSessionReset(senderUuid: string, senderDevice: number) {
|
||||
// Postpone sending light session resets until the queue is empty
|
||||
lightSessionResetQueue.add(() => {
|
||||
window.textsecure.storage.protocol.lightSessionReset(
|
||||
|
@ -3306,6 +3615,12 @@ export async function startApp(): Promise<void> {
|
|||
senderDevice
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function startAutomaticSessionReset(decryptionError: DecryptionErrorType) {
|
||||
const { senderUuid, senderDevice } = decryptionError;
|
||||
|
||||
scheduleSessionReset(senderUuid, senderDevice);
|
||||
|
||||
const conversationId = window.ConversationController.ensureContactIds({
|
||||
uuid: senderUuid,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Modal } from '../Modal';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
|
@ -12,13 +14,13 @@ export type PropsType = {
|
|||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
// TODO: This should use <Modal>. See DESKTOP-1038.
|
||||
export function ChatSessionRefreshedDialog(
|
||||
props: PropsType
|
||||
): React.ReactElement {
|
||||
const { i18n, contactSupport, onClose } = props;
|
||||
|
||||
return (
|
||||
<Modal hasXButton={false} i18n={i18n}>
|
||||
<div className="module-chat-session-refreshed-dialog">
|
||||
<div className="module-chat-session-refreshed-dialog__image">
|
||||
<img
|
||||
|
@ -54,5 +56,6 @@ export function ChatSessionRefreshedDialog(
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import React, { useCallback, useState, ReactElement } from 'react';
|
|||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { ModalHost } from '../ModalHost';
|
||||
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
|
@ -50,13 +49,11 @@ export function ChatSessionRefreshedNotification(
|
|||
{i18n('ChatRefresh--learnMore')}
|
||||
</button>
|
||||
{isDialogOpen ? (
|
||||
<ModalHost onClose={closeDialog}>
|
||||
<ChatSessionRefreshedDialog
|
||||
onClose={closeDialog}
|
||||
contactSupport={wrappedContactSupport}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</ModalHost>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
27
ts/components/conversation/DeliveryIssueDialog.stories.tsx
Normal file
27
ts/components/conversation/DeliveryIssueDialog.stories.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { DeliveryIssueDialog } from './DeliveryIssueDialog';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
const sender = getDefaultConversation();
|
||||
|
||||
storiesOf('Components/Conversation/DeliveryIssueDialog', module).add(
|
||||
'Default',
|
||||
() => {
|
||||
return (
|
||||
<DeliveryIssueDialog
|
||||
i18n={i18n}
|
||||
sender={sender}
|
||||
onClose={action('onClose')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
57
ts/components/conversation/DeliveryIssueDialog.tsx
Normal file
57
ts/components/conversation/DeliveryIssueDialog.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { Modal } from '../Modal';
|
||||
import { Intl } from '../Intl';
|
||||
import { Emojify } from './Emojify';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
sender: ConversationType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
|
||||
const { i18n, sender, onClose } = props;
|
||||
|
||||
return (
|
||||
<Modal hasXButton={false} i18n={i18n}>
|
||||
<div className="module-delivery-issue-dialog">
|
||||
<div className="module-delivery-issue-dialog__image">
|
||||
<img
|
||||
src="images/delivery-issue.svg"
|
||||
height="110"
|
||||
width="200"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="module-delivery-issue-dialog__title">
|
||||
{i18n('DeliveryIssue--title')}
|
||||
</div>
|
||||
<div className="module-delivery-issue-dialog__description">
|
||||
<Intl
|
||||
id="DeliveryIssue--summary"
|
||||
components={{
|
||||
sender: <Emojify text={sender.title} />,
|
||||
}}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-delivery-issue-dialog__buttons">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="module-delivery-issue-dialog__button"
|
||||
>
|
||||
{i18n('Confirmation--confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { DeliveryIssueNotification } from './DeliveryIssueNotification';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
const sender = getDefaultConversation();
|
||||
|
||||
storiesOf('Components/Conversation/DeliveryIssueNotification', module).add(
|
||||
'Default',
|
||||
() => {
|
||||
return <DeliveryIssueNotification i18n={i18n} sender={sender} />;
|
||||
}
|
||||
);
|
68
ts/components/conversation/DeliveryIssueNotification.tsx
Normal file
68
ts/components/conversation/DeliveryIssueNotification.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useState, ReactElement } from 'react';
|
||||
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { Intl } from '../Intl';
|
||||
import { Emojify } from './Emojify';
|
||||
|
||||
import { DeliveryIssueDialog } from './DeliveryIssueDialog';
|
||||
|
||||
export type PropsDataType = {
|
||||
sender?: ConversationType;
|
||||
};
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType & PropsHousekeepingType;
|
||||
|
||||
export function DeliveryIssueNotification(
|
||||
props: PropsType
|
||||
): ReactElement | null {
|
||||
const { i18n, sender } = props;
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const openDialog = useCallback(() => {
|
||||
setIsDialogOpen(true);
|
||||
}, [setIsDialogOpen]);
|
||||
const closeDialog = useCallback(() => {
|
||||
setIsDialogOpen(false);
|
||||
}, [setIsDialogOpen]);
|
||||
|
||||
if (!sender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-delivery-issue-notification">
|
||||
<div className="module-delivery-issue-notification__first-line">
|
||||
<span className="module-delivery-issue-notification__icon" />
|
||||
<Intl
|
||||
id="DeliveryIssue--notification"
|
||||
components={{
|
||||
sender: <Emojify text={sender.firstName || sender.title} />,
|
||||
}}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openDialog}
|
||||
className="module-delivery-issue-notification__button"
|
||||
>
|
||||
{i18n('DeliveryIssue--learnMore')}
|
||||
</button>
|
||||
{isDialogOpen ? (
|
||||
<DeliveryIssueDialog
|
||||
i18n={i18n}
|
||||
sender={sender}
|
||||
onClose={closeDialog}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -11,6 +11,7 @@ import { setup as setupI18n } from '../../../js/modules/i18n';
|
|||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -99,9 +100,19 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
|||
{
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
expireTimer: 60,
|
||||
...getDefaultConversation(),
|
||||
type: 'fromOther',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'chatSessionRefreshed',
|
||||
},
|
||||
{
|
||||
type: 'deliveryIssue',
|
||||
data: {
|
||||
sender: getDefaultConversation(),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -367,7 +378,6 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
|||
item={item as TimelineItemProps['item']}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -19,6 +19,10 @@ import {
|
|||
ChatSessionRefreshedNotification,
|
||||
PropsActionsType as PropsChatSessionRefreshedActionsType,
|
||||
} from './ChatSessionRefreshedNotification';
|
||||
import {
|
||||
DeliveryIssueNotification,
|
||||
PropsDataType as DeliveryIssueProps,
|
||||
} from './DeliveryIssueNotification';
|
||||
import { CallingNotificationType } from '../../util/callingNotification';
|
||||
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
|
||||
import {
|
||||
|
@ -66,6 +70,10 @@ type ChatSessionRefreshedType = {
|
|||
type: 'chatSessionRefreshed';
|
||||
data: null;
|
||||
};
|
||||
type DeliveryIssueType = {
|
||||
type: 'deliveryIssue';
|
||||
data: DeliveryIssueProps;
|
||||
};
|
||||
type LinkNotificationType = {
|
||||
type: 'linkNotification';
|
||||
data: null;
|
||||
|
@ -114,6 +122,7 @@ type ProfileChangeNotificationType = {
|
|||
export type TimelineItemType =
|
||||
| CallHistoryType
|
||||
| ChatSessionRefreshedType
|
||||
| DeliveryIssueType
|
||||
| GroupNotificationType
|
||||
| GroupV1MigrationType
|
||||
| GroupV2ChangeType
|
||||
|
@ -203,6 +212,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'deliveryIssue') {
|
||||
notification = <DeliveryIssueNotification {...item.data} i18n={i18n} />;
|
||||
} else if (item.type === 'linkNotification') {
|
||||
notification = (
|
||||
<div className="module-message-unsynced">
|
||||
|
|
21
ts/groups.ts
21
ts/groups.ts
|
@ -1259,6 +1259,9 @@ export async function modifyGroupV2({
|
|||
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
const timestamp = Date.now();
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const promise = conversation.wrapSend(
|
||||
window.Signal.Util.sendToGroup(
|
||||
|
@ -1272,6 +1275,7 @@ export async function modifyGroupV2({
|
|||
profileKey,
|
||||
},
|
||||
conversation,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
sendOptions
|
||||
)
|
||||
);
|
||||
|
@ -1629,6 +1633,10 @@ export async function createGroupV2({
|
|||
const groupV2Info = conversation.getGroupV2Info({
|
||||
includePendingMembers: true,
|
||||
});
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
|
||||
await wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
|
@ -1640,7 +1648,9 @@ export async function createGroupV2({
|
|||
timestamp,
|
||||
profileKey,
|
||||
},
|
||||
conversation
|
||||
conversation,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
sendOptions
|
||||
),
|
||||
timestamp,
|
||||
});
|
||||
|
@ -2145,6 +2155,11 @@ export async function initiateMigrationToGroupV2(
|
|||
| ArrayBuffer
|
||||
| undefined = await ourProfileKeyService.get();
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
|
||||
await wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId: `sendToGroup/${logId}`,
|
||||
|
@ -2158,7 +2173,9 @@ export async function initiateMigrationToGroupV2(
|
|||
timestamp,
|
||||
profileKey: ourProfileKey,
|
||||
},
|
||||
conversation
|
||||
conversation,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
sendOptions
|
||||
),
|
||||
timestamp,
|
||||
});
|
||||
|
|
15
ts/model-types.d.ts
vendored
15
ts/model-types.d.ts
vendored
|
@ -129,18 +129,19 @@ export type MessageAttributesType = {
|
|||
|
||||
id: string;
|
||||
type?:
|
||||
| 'incoming'
|
||||
| 'outgoing'
|
||||
| 'group'
|
||||
| 'keychange'
|
||||
| 'verified-change'
|
||||
| 'message-history-unsynced'
|
||||
| 'call-history'
|
||||
| 'chat-session-refreshed'
|
||||
| 'delivery-issue'
|
||||
| 'group'
|
||||
| 'group-v1-migration'
|
||||
| 'group-v2-change'
|
||||
| 'incoming'
|
||||
| 'keychange'
|
||||
| 'message-history-unsynced'
|
||||
| 'outgoing'
|
||||
| 'profile-change'
|
||||
| 'timer-notification';
|
||||
| 'timer-notification'
|
||||
| 'verified-change';
|
||||
body: string;
|
||||
attachments: Array<WhatIsThis>;
|
||||
preview: Array<WhatIsThis>;
|
||||
|
|
|
@ -139,6 +139,8 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
throttledFetchSMSOnlyUUID?: () => Promise<void> | void;
|
||||
|
||||
throttledMaybeMigrateV1Group?: () => Promise<void> | void;
|
||||
|
||||
typingRefreshTimer?: NodeJS.Timer | null;
|
||||
|
||||
typingPauseTimer?: NodeJS.Timer | null;
|
||||
|
@ -304,7 +306,11 @@ export class ConversationModel extends window.Backbone
|
|||
this.isFetchingUUID = this.isSMSOnly();
|
||||
|
||||
this.throttledFetchSMSOnlyUUID = window._.throttle(
|
||||
this.fetchSMSOnlyUUID,
|
||||
this.fetchSMSOnlyUUID.bind(this),
|
||||
FIVE_MINUTES
|
||||
);
|
||||
this.throttledMaybeMigrateV1Group = window._.throttle(
|
||||
this.maybeMigrateV1Group.bind(this),
|
||||
FIVE_MINUTES
|
||||
);
|
||||
|
||||
|
@ -811,6 +817,10 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
setRegistered(): void {
|
||||
if (this.get('discoveredUnregisteredAt') === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
`Conversation ${this.idForLogging()} is registered once again`
|
||||
);
|
||||
|
@ -1193,15 +1203,18 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
);
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const sendOptions = await this.getSendOptions();
|
||||
if (this.isPrivate()) {
|
||||
const silent = true;
|
||||
this.wrapSend(
|
||||
window.textsecure.messaging.sendMessageProtoAndWait(
|
||||
timestamp,
|
||||
groupMembers,
|
||||
contentMessage,
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined,
|
||||
{
|
||||
...sendOptions,
|
||||
online: true,
|
||||
|
@ -1211,6 +1224,7 @@ export class ConversationModel extends window.Backbone
|
|||
} else {
|
||||
this.wrapSend(
|
||||
window.Signal.Util.sendContentMessageToGroup({
|
||||
contentHint: ContentHint.SUPPLEMENTARY,
|
||||
contentMessage,
|
||||
conversation: this,
|
||||
online: true,
|
||||
|
@ -2438,7 +2452,8 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
async addChatSessionRefreshed(receivedAt: number): Promise<void> {
|
||||
window.log.info(
|
||||
`addChatSessionRefreshed: adding for ${this.idForLogging()}`
|
||||
`addChatSessionRefreshed: adding for ${this.idForLogging()}`,
|
||||
{ receivedAt }
|
||||
);
|
||||
|
||||
const message = ({
|
||||
|
@ -2466,6 +2481,43 @@ export class ConversationModel extends window.Backbone
|
|||
this.trigger('newmessage', model);
|
||||
}
|
||||
|
||||
async addDeliveryIssue(
|
||||
receivedAt: number,
|
||||
senderUuid: string
|
||||
): Promise<void> {
|
||||
window.log.info(`addDeliveryIssue: adding for ${this.idForLogging()}`, {
|
||||
receivedAt,
|
||||
senderUuid,
|
||||
});
|
||||
|
||||
const message = ({
|
||||
conversationId: this.id,
|
||||
type: 'delivery-issue',
|
||||
sourceUuid: senderUuid,
|
||||
sent_at: receivedAt,
|
||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||
received_at_ms: receivedAt,
|
||||
unread: 1,
|
||||
// TODO: DESKTOP-722
|
||||
// this type does not fully implement the interface it is expected to
|
||||
} as unknown) as typeof window.Whisper.MessageAttributesType;
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
Message: window.Whisper.Message,
|
||||
});
|
||||
const model = window.MessageController.register(
|
||||
id,
|
||||
new window.Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
})
|
||||
);
|
||||
|
||||
this.trigger('newmessage', model);
|
||||
|
||||
await this.notify(model);
|
||||
}
|
||||
|
||||
async addKeyChange(keyChangedId: string): Promise<void> {
|
||||
window.log.info(
|
||||
'adding key change advisory for',
|
||||
|
@ -3108,6 +3160,10 @@ export class ConversationModel extends window.Backbone
|
|||
profileKey = await ourProfileKeyService.get();
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
if (this.isPrivate()) {
|
||||
return window.textsecure.messaging.sendMessageToIdentifier(
|
||||
destination,
|
||||
|
@ -3120,6 +3176,8 @@ export class ConversationModel extends window.Backbone
|
|||
targetTimestamp,
|
||||
timestamp,
|
||||
undefined, // expireTimer
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // groupId
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
|
@ -3134,6 +3192,7 @@ export class ConversationModel extends window.Backbone
|
|||
profileKey,
|
||||
},
|
||||
this,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
})();
|
||||
|
@ -3254,6 +3313,9 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
const options = await this.getSendOptions();
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const promise = (() => {
|
||||
if (this.isPrivate()) {
|
||||
|
@ -3268,6 +3330,8 @@ export class ConversationModel extends window.Backbone
|
|||
undefined, // deletedForEveryoneTimestamp
|
||||
timestamp,
|
||||
expireTimer,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // groupId
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
|
@ -3285,6 +3349,7 @@ export class ConversationModel extends window.Backbone
|
|||
profileKey,
|
||||
},
|
||||
this,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
})();
|
||||
|
@ -3492,6 +3557,9 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
const conversationType = this.get('type');
|
||||
const options = await this.getSendOptions();
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
let promise;
|
||||
if (conversationType === Message.GROUP) {
|
||||
|
@ -3510,6 +3578,7 @@ export class ConversationModel extends window.Backbone
|
|||
mentions,
|
||||
},
|
||||
this,
|
||||
ContentHint.RESENDABLE,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
|
@ -3524,6 +3593,8 @@ export class ConversationModel extends window.Backbone
|
|||
undefined, // deletedForEveryoneTimestamp
|
||||
now,
|
||||
expireTimer,
|
||||
ContentHint.RESENDABLE,
|
||||
undefined, // groupId
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
import { PropsData as SafetyNumberNotificationProps } from '../components/conversation/SafetyNumberNotification';
|
||||
import { PropsData as VerificationNotificationProps } from '../components/conversation/VerificationNotification';
|
||||
import { PropsDataType as GroupV1MigrationPropsType } from '../components/conversation/GroupV1Migration';
|
||||
import { PropsDataType as DeliveryIssuePropsType } from '../components/conversation/DeliveryIssueNotification';
|
||||
import {
|
||||
PropsData as GroupNotificationProps,
|
||||
ChangeType,
|
||||
|
@ -132,6 +133,10 @@ type MessageBubbleProps =
|
|||
type: 'chatSessionRefreshed';
|
||||
data: null;
|
||||
}
|
||||
| {
|
||||
type: 'deliveryIssue';
|
||||
data: DeliveryIssuePropsType;
|
||||
}
|
||||
| {
|
||||
type: 'message';
|
||||
data: PropsForMessage;
|
||||
|
@ -407,6 +412,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
data: null,
|
||||
};
|
||||
}
|
||||
if (this.isDeliveryIssue()) {
|
||||
return {
|
||||
type: 'deliveryIssue',
|
||||
data: this.getPropsForDeliveryIssue(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
|
@ -581,6 +592,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return this.get('type') === 'chat-session-refreshed';
|
||||
}
|
||||
|
||||
isDeliveryIssue(): boolean {
|
||||
return this.get('type') === 'delivery-issue';
|
||||
}
|
||||
|
||||
isProfileChange(): boolean {
|
||||
return this.get('type') === 'profile-change';
|
||||
}
|
||||
|
@ -874,6 +889,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
}
|
||||
|
||||
getPropsForDeliveryIssue(): DeliveryIssuePropsType {
|
||||
const sender = this.getContact()?.format();
|
||||
|
||||
return {
|
||||
sender,
|
||||
};
|
||||
}
|
||||
|
||||
getPropsForProfileChange(): ProfileChangeNotificationPropsType {
|
||||
const change = this.get('profileChange');
|
||||
const changedId = this.get('changedId');
|
||||
|
@ -1359,6 +1382,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
getNotificationData(): { emoji?: string; text: string } {
|
||||
if (this.isDeliveryIssue()) {
|
||||
return {
|
||||
emoji: '⚠️',
|
||||
text: window.i18n('DeliveryIssue--preview'),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isChatSessionRefreshed()) {
|
||||
return {
|
||||
emoji: '🔁',
|
||||
|
@ -1893,6 +1923,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
// Rendered sync messages
|
||||
const isCallHistory = this.isCallHistory();
|
||||
const isChatSessionRefreshed = this.isChatSessionRefreshed();
|
||||
const isDeliveryIssue = this.isDeliveryIssue();
|
||||
const isGroupUpdate = this.isGroupUpdate();
|
||||
const isGroupV2Change = this.isGroupV2Change();
|
||||
const isEndSession = this.isEndSession();
|
||||
|
@ -1922,6 +1953,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
// Rendered sync messages
|
||||
isCallHistory ||
|
||||
isChatSessionRefreshed ||
|
||||
isDeliveryIssue ||
|
||||
isGroupUpdate ||
|
||||
isGroupV2Change ||
|
||||
isEndSession ||
|
||||
|
@ -2216,6 +2248,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
let promise;
|
||||
const options = await conversation.getSendOptions();
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
if (conversation.isPrivate()) {
|
||||
const [identifier] = recipients;
|
||||
promise = window.textsecure.messaging.sendMessageToIdentifier(
|
||||
|
@ -2229,6 +2265,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
this.get('deletedForEveryoneTimestamp'),
|
||||
this.get('sent_at'),
|
||||
this.get('expireTimer'),
|
||||
ContentHint.RESENDABLE,
|
||||
undefined, // groupId
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
|
@ -2271,6 +2309,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
groupV1,
|
||||
},
|
||||
conversation,
|
||||
ContentHint.RESENDABLE,
|
||||
options,
|
||||
partialSend
|
||||
);
|
||||
|
@ -2403,7 +2442,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
async resend(identifier: string): Promise<void | null | Array<void>> {
|
||||
const error = this.removeOutgoingErrors(identifier);
|
||||
if (!error) {
|
||||
window.log.warn('resend: requested number was not present in errors');
|
||||
window.log.warn(
|
||||
'resend: requested number was not present in errors. continuing.'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isErased()) {
|
||||
window.log.warn('resend: message is erased; refusing to resend');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -2431,7 +2476,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
body,
|
||||
deletedForEveryoneTimestamp: this.get('deletedForEveryoneTimestamp'),
|
||||
expireTimer: this.get('expireTimer'),
|
||||
// flags
|
||||
mentions: this.get('bodyRanges'),
|
||||
preview: previewWithData,
|
||||
profileKey,
|
||||
|
@ -2444,22 +2488,59 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return this.sendSyncMessageOnly(dataMessage);
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const parentConversation = this.getConversation();
|
||||
const groupId = parentConversation?.get('groupId');
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(identifier);
|
||||
const promise = window.textsecure.messaging.sendMessageToIdentifier(
|
||||
identifier,
|
||||
body,
|
||||
const group =
|
||||
groupId && parentConversation?.isGroupV1()
|
||||
? {
|
||||
id: groupId,
|
||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const timestamp = this.get('sent_at');
|
||||
const contentMessage = await window.textsecure.messaging.getContentMessage({
|
||||
attachments,
|
||||
quoteWithData,
|
||||
previewWithData,
|
||||
stickerWithData,
|
||||
null,
|
||||
this.get('deletedForEveryoneTimestamp'),
|
||||
this.get('sent_at'),
|
||||
this.get('expireTimer'),
|
||||
profileKey,
|
||||
body,
|
||||
expireTimer: this.get('expireTimer'),
|
||||
group,
|
||||
groupV2: parentConversation?.getGroupV2Info(),
|
||||
preview: previewWithData,
|
||||
quote: quoteWithData,
|
||||
mentions: this.get('bodyRanges'),
|
||||
recipients: [identifier],
|
||||
sticker: stickerWithData,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
if (parentConversation) {
|
||||
const senderKeyInfo = parentConversation.get('senderKeyInfo');
|
||||
if (senderKeyInfo && senderKeyInfo.distributionId) {
|
||||
const senderKeyDistributionMessage = await window.textsecure.messaging.getSenderKeyDistributionMessage(
|
||||
senderKeyInfo.distributionId
|
||||
);
|
||||
|
||||
window.dcodeIO.ByteBuffer.wrap(
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
senderKeyDistributionMessage.serialize()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const promise = window.textsecure.messaging.sendMessageProtoAndWait(
|
||||
timestamp,
|
||||
[identifier],
|
||||
contentMessage,
|
||||
ContentHint.RESENDABLE,
|
||||
groupId && parentConversation?.isGroupV2() ? groupId : undefined,
|
||||
sendOptions
|
||||
);
|
||||
|
||||
|
@ -2506,7 +2587,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
sent_to: _.union(sentTo, result.successfulIdentifiers),
|
||||
sent: true,
|
||||
expirationStartTimestamp: Date.now(),
|
||||
unidentifiedDeliveries: result.unidentifiedDeliveries,
|
||||
unidentifiedDeliveries: _.union(
|
||||
this.get('unidentifiedDeliveries') || [],
|
||||
result.unidentifiedDeliveries
|
||||
),
|
||||
});
|
||||
|
||||
if (!this.doNotSave) {
|
||||
|
@ -2595,7 +2679,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
sent_to: _.union(sentTo, result.successfulIdentifiers),
|
||||
sent: true,
|
||||
expirationStartTimestamp,
|
||||
unidentifiedDeliveries: result.unidentifiedDeliveries,
|
||||
unidentifiedDeliveries: _.union(
|
||||
this.get('unidentifiedDeliveries') || [],
|
||||
result.unidentifiedDeliveries
|
||||
),
|
||||
});
|
||||
promises.push(this.sendSyncMessage());
|
||||
} else if (result.errors) {
|
||||
|
@ -3452,6 +3539,24 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
}
|
||||
|
||||
// Now check for decryption error placeholders
|
||||
const { retryPlaceholders } = window.Signal.Services;
|
||||
if (retryPlaceholders) {
|
||||
const item = await retryPlaceholders.findByMessageAndRemove(
|
||||
conversationId,
|
||||
message.get('sent_at')
|
||||
);
|
||||
if (item) {
|
||||
window.log.info(
|
||||
`handleDataMessage: found retry placeholder. Updating ${message.idForLogging()} received_at/received_at_ms`
|
||||
);
|
||||
message.set({
|
||||
received_at: item.receivedAtCounter,
|
||||
received_at_ms: item.receivedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// GroupV2
|
||||
|
||||
if (initialMessage.groupV2) {
|
||||
|
|
|
@ -766,6 +766,9 @@ export class CallingClass {
|
|||
const timestamp = Date.now();
|
||||
|
||||
// We "fire and forget" because sending this message is non-essential.
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`,
|
||||
|
@ -773,6 +776,7 @@ export class CallingClass {
|
|||
window.Signal.Util.sendToGroup(
|
||||
{ groupCallUpdate: { eraId }, groupV2, timestamp },
|
||||
conversation,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
sendOptions
|
||||
),
|
||||
timestamp,
|
||||
|
|
|
@ -159,18 +159,19 @@ export type MessageType = {
|
|||
source?: string;
|
||||
sourceUuid?: string;
|
||||
type?:
|
||||
| 'incoming'
|
||||
| 'outgoing'
|
||||
| 'group'
|
||||
| 'keychange'
|
||||
| 'verified-change'
|
||||
| 'message-history-unsynced'
|
||||
| 'call-history'
|
||||
| 'chat-session-refreshed'
|
||||
| 'delivery-issue'
|
||||
| 'group'
|
||||
| 'group-v1-migration'
|
||||
| 'group-v2-change'
|
||||
| 'incoming'
|
||||
| 'keychange'
|
||||
| 'message-history-unsynced'
|
||||
| 'outgoing'
|
||||
| 'profile-change'
|
||||
| 'timer-notification';
|
||||
| 'timer-notification'
|
||||
| 'verified-change';
|
||||
quote?: { author?: string; authorUuid?: string };
|
||||
received_at: number;
|
||||
sent_at?: number;
|
||||
|
|
285
ts/test-both/util/retryPlaceholders_test.ts
Normal file
285
ts/test-both/util/retryPlaceholders_test.ts
Normal file
|
@ -0,0 +1,285 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import {
|
||||
getOneHourAgo,
|
||||
RetryItemType,
|
||||
RetryPlaceholders,
|
||||
STORAGE_KEY,
|
||||
} from '../../util/retryPlaceholders';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('RetryPlaceholders', () => {
|
||||
beforeEach(() => {
|
||||
window.storage.put(STORAGE_KEY, null);
|
||||
});
|
||||
|
||||
function getDefaultItem(): RetryItemType {
|
||||
return {
|
||||
conversationId: 'conversation-id',
|
||||
sentAt: Date.now() - 10,
|
||||
receivedAt: Date.now() - 5,
|
||||
receivedAtCounter: 4,
|
||||
senderUuid: 'sender-uuid',
|
||||
};
|
||||
}
|
||||
|
||||
describe('constructor', () => {
|
||||
it('loads previously-saved data on creation', () => {
|
||||
const items: Array<RetryItemType> = [
|
||||
getDefaultItem(),
|
||||
{ ...getDefaultItem(), conversationId: 'conversation-id-2' },
|
||||
];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
});
|
||||
it('starts with no data if provided data fails to parse', () => {
|
||||
window.storage.put(STORAGE_KEY, [
|
||||
{ item: 'is wrong shape!' },
|
||||
{ bad: 'is not good!' },
|
||||
]);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
|
||||
assert.strictEqual(0, placeholders.getCount());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#add', () => {
|
||||
it('adds one item', async () => {
|
||||
const placeholders = new RetryPlaceholders();
|
||||
await placeholders.add(getDefaultItem());
|
||||
assert.strictEqual(1, placeholders.getCount());
|
||||
});
|
||||
|
||||
it('throws if provided data fails to parse', () => {
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.isRejected(
|
||||
placeholders.add({
|
||||
item: 'is wrong shape!',
|
||||
} as any),
|
||||
'Item did not match schema'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getNextToExpire', () => {
|
||||
it('returns nothing if no items', () => {
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(0, placeholders.getCount());
|
||||
assert.isUndefined(placeholders.getNextToExpire());
|
||||
});
|
||||
it('returns only item if just one item', () => {
|
||||
const item = getDefaultItem();
|
||||
const items: Array<RetryItemType> = [item];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(1, placeholders.getCount());
|
||||
assert.deepEqual(item, placeholders.getNextToExpire());
|
||||
});
|
||||
it('returns soonest expiration given a list, and after add', async () => {
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: Date.now(),
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: Date.now() + 10,
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual(older, placeholders.getNextToExpire());
|
||||
|
||||
const oldest = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: Date.now() - 5,
|
||||
};
|
||||
|
||||
await placeholders.add(oldest);
|
||||
assert.strictEqual(3, placeholders.getCount());
|
||||
assert.deepEqual(oldest, placeholders.getNextToExpire());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getExpiredAndRemove', () => {
|
||||
it('does nothing if no item expired', async () => {
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: Date.now() + 10,
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: Date.now() + 15,
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual([], await placeholders.getExpiredAndRemove());
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
});
|
||||
it('removes just one if expired', async () => {
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: getOneHourAgo() - 1000,
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: Date.now() + 15,
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual([older], await placeholders.getExpiredAndRemove());
|
||||
assert.strictEqual(1, placeholders.getCount());
|
||||
assert.deepEqual(newer, placeholders.getNextToExpire());
|
||||
});
|
||||
it('removes all if expired', async () => {
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: getOneHourAgo() - 1000,
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
receivedAt: getOneHourAgo() - 900,
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual(
|
||||
[older, newer],
|
||||
await placeholders.getExpiredAndRemove()
|
||||
);
|
||||
assert.strictEqual(0, placeholders.getCount());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findByConversationAndRemove', () => {
|
||||
it('does nothing if no items found matching conversation', async () => {
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-2',
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual(
|
||||
[],
|
||||
await placeholders.findByConversationAndRemove('conversation-id-3')
|
||||
);
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
});
|
||||
it('removes all items matching conversation', async () => {
|
||||
const convo1a = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
receivedAt: Date.now() - 5,
|
||||
};
|
||||
const convo1b = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
receivedAt: Date.now() - 4,
|
||||
};
|
||||
const convo2a = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-2',
|
||||
receivedAt: Date.now() + 15,
|
||||
};
|
||||
const items: Array<RetryItemType> = [convo1a, convo1b, convo2a];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(3, placeholders.getCount());
|
||||
assert.deepEqual(
|
||||
[convo1a, convo1b],
|
||||
await placeholders.findByConversationAndRemove('conversation-id-1')
|
||||
);
|
||||
assert.strictEqual(1, placeholders.getCount());
|
||||
|
||||
const convo2b = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-2',
|
||||
receivedAt: Date.now() + 16,
|
||||
};
|
||||
|
||||
await placeholders.add(convo2b);
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual(
|
||||
[convo2a, convo2b],
|
||||
await placeholders.findByConversationAndRemove('conversation-id-2')
|
||||
);
|
||||
assert.strictEqual(0, placeholders.getCount());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findByMessageAndRemove', () => {
|
||||
it('does nothing if no item matching message found', async () => {
|
||||
const sentAt = Date.now() - 20;
|
||||
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
sentAt: Date.now() - 10,
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
sentAt: Date.now() - 11,
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.isUndefined(
|
||||
await placeholders.findByMessageAndRemove('conversation-id-1', sentAt)
|
||||
);
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
});
|
||||
it('removes the item matching message', async () => {
|
||||
const sentAt = Date.now() - 20;
|
||||
|
||||
const older = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
sentAt: Date.now() - 10,
|
||||
};
|
||||
const newer = {
|
||||
...getDefaultItem(),
|
||||
conversationId: 'conversation-id-1',
|
||||
sentAt,
|
||||
};
|
||||
const items: Array<RetryItemType> = [older, newer];
|
||||
window.storage.put(STORAGE_KEY, items);
|
||||
|
||||
const placeholders = new RetryPlaceholders();
|
||||
assert.strictEqual(2, placeholders.getCount());
|
||||
assert.deepEqual(
|
||||
newer,
|
||||
await placeholders.findByMessageAndRemove('conversation-id-1', sentAt)
|
||||
);
|
||||
assert.strictEqual(1, placeholders.getCount());
|
||||
});
|
||||
});
|
||||
});
|
57
ts/test-electron/background_test.ts
Normal file
57
ts/test-electron/background_test.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isOverHourIntoPast, cleanupSessionResets } from '../background';
|
||||
|
||||
describe('#isOverHourIntoPast', () => {
|
||||
it('returns false for now', () => {
|
||||
assert.isFalse(isOverHourIntoPast(Date.now()));
|
||||
});
|
||||
it('returns false for 5 minutes ago', () => {
|
||||
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
||||
assert.isFalse(isOverHourIntoPast(fiveMinutesAgo));
|
||||
});
|
||||
it('returns true for 65 minutes ago', () => {
|
||||
const sixtyFiveMinutesAgo = Date.now() - 65 * 60 * 1000;
|
||||
assert.isTrue(isOverHourIntoPast(sixtyFiveMinutesAgo));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#cleanupSessionResets', () => {
|
||||
it('leaves empty object alone', () => {
|
||||
window.storage.put('sessionResets', {});
|
||||
cleanupSessionResets();
|
||||
const actual = window.storage.get('sessionResets');
|
||||
|
||||
const expected = {};
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
it('filters out any timestamp older than one hour', () => {
|
||||
const startValue = {
|
||||
one: Date.now() - 1,
|
||||
two: Date.now(),
|
||||
three: Date.now() - 65 * 60 * 1000,
|
||||
};
|
||||
window.storage.put('sessionResets', startValue);
|
||||
cleanupSessionResets();
|
||||
const actual = window.storage.get('sessionResets');
|
||||
|
||||
const expected = window._.pick(startValue, ['one', 'two']);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
it('filters out falsey items', () => {
|
||||
const startValue = {
|
||||
one: 0,
|
||||
two: false,
|
||||
three: Date.now(),
|
||||
};
|
||||
window.storage.put('sessionResets', startValue);
|
||||
cleanupSessionResets();
|
||||
const actual = window.storage.get('sessionResets');
|
||||
|
||||
const expected = window._.pick(startValue, ['three']);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
10
ts/textsecure.d.ts
vendored
10
ts/textsecure.d.ts
vendored
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { UnidentifiedSenderMessageContent } from '@signalapp/signal-client';
|
||||
|
||||
import Crypto from './textsecure/Crypto';
|
||||
import MessageReceiver from './textsecure/MessageReceiver';
|
||||
import MessageSender from './textsecure/SendMessage';
|
||||
|
@ -571,6 +573,7 @@ export declare class ContentClass {
|
|||
receiptMessage?: ReceiptMessageClass;
|
||||
typingMessage?: TypingMessageClass;
|
||||
senderKeyDistributionMessage?: ByteBufferClass;
|
||||
decryptionErrorMessage?: ByteBufferClass;
|
||||
}
|
||||
|
||||
export declare class DataMessageClass {
|
||||
|
@ -722,6 +725,9 @@ export declare class EnvelopeClass {
|
|||
receivedAtDate: number;
|
||||
unidentifiedDeliveryReceived?: boolean;
|
||||
messageAgeSec?: number;
|
||||
contentHint?: number;
|
||||
groupId?: string;
|
||||
usmc?: UnidentifiedSenderMessageContent;
|
||||
}
|
||||
|
||||
// Note: we need to use namespaces to express nested classes in Typescript
|
||||
|
@ -731,6 +737,7 @@ export declare namespace EnvelopeClass {
|
|||
static PREKEY_BUNDLE: number;
|
||||
static RECEIPT: number;
|
||||
static UNIDENTIFIED_SENDER: number;
|
||||
static PLAINTEXT_CONTENT: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1386,10 +1393,11 @@ export declare namespace UnidentifiedSenderMessageClass.Message {
|
|||
static PREKEY_MESSAGE: number;
|
||||
static MESSAGE: number;
|
||||
static SENDERKEY_MESSAGE: number;
|
||||
static PLAINTEXT_CONTENT: number;
|
||||
}
|
||||
|
||||
class ContentHint {
|
||||
static SUPPLEMENTARY: number;
|
||||
static RETRY: number;
|
||||
static RESENDABLE: number;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,8 +70,8 @@ export class OutgoingMessageError extends ReplayableError {
|
|||
// Note: Data to resend message is no longer captured
|
||||
constructor(
|
||||
incomingIdentifier: string,
|
||||
_m: ArrayBuffer,
|
||||
_t: number,
|
||||
_m: unknown,
|
||||
_t: unknown,
|
||||
httpError?: Error
|
||||
) {
|
||||
const identifier = incomingIdentifier.split('.')[0];
|
||||
|
|
|
@ -13,9 +13,12 @@
|
|||
import { isNumber, map, omit, noop } from 'lodash';
|
||||
import PQueue from 'p-queue';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
DecryptionErrorMessage,
|
||||
groupDecrypt,
|
||||
PlaintextContent,
|
||||
PreKeySignalMessage,
|
||||
processSenderKeyDistributionMessage,
|
||||
ProtocolAddress,
|
||||
|
@ -73,7 +76,30 @@ const GROUPV1_ID_LENGTH = 16;
|
|||
const GROUPV2_ID_LENGTH = 32;
|
||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||
|
||||
type SessionResetsType = Record<string, number>;
|
||||
const decryptionErrorTypeSchema = z
|
||||
.object({
|
||||
cipherTextBytes: z.instanceof(ArrayBuffer).optional(),
|
||||
cipherTextType: z.number().optional(),
|
||||
contentHint: z.number().optional(),
|
||||
groupId: z.string().optional(),
|
||||
receivedAtCounter: z.number(),
|
||||
receivedAtDate: z.number(),
|
||||
senderDevice: z.number(),
|
||||
senderUuid: z.string(),
|
||||
timestamp: z.number(),
|
||||
})
|
||||
.passthrough();
|
||||
export type DecryptionErrorType = z.infer<typeof decryptionErrorTypeSchema>;
|
||||
|
||||
const retryRequestTypeSchema = z
|
||||
.object({
|
||||
requesterUuid: z.string(),
|
||||
requesterDevice: z.number(),
|
||||
senderDevice: z.number(),
|
||||
sentAt: z.number(),
|
||||
})
|
||||
.passthrough();
|
||||
export type RetryRequestType = z.infer<typeof retryRequestTypeSchema>;
|
||||
|
||||
declare global {
|
||||
// We want to extend `Event`, so we need an interface.
|
||||
|
@ -107,6 +133,8 @@ declare global {
|
|||
timestamp?: any;
|
||||
typing?: any;
|
||||
verified?: any;
|
||||
retryRequest?: RetryRequestType;
|
||||
decryptionError?: DecryptionErrorType;
|
||||
}
|
||||
// We want to extend `Error`, so we need an interface.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
@ -261,8 +289,6 @@ class MessageReceiverInner extends EventTarget {
|
|||
maxSize: 30,
|
||||
processBatch: this.cacheRemoveBatch.bind(this),
|
||||
});
|
||||
|
||||
this.cleanupSessionResets();
|
||||
}
|
||||
|
||||
static stringToArrayBuffer = (string: string): ArrayBuffer =>
|
||||
|
@ -1122,7 +1148,14 @@ class MessageReceiverInner extends EventTarget {
|
|||
ArrayBuffer | { isMe: boolean } | { isBlocked: boolean } | undefined
|
||||
>;
|
||||
|
||||
if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
|
||||
if (envelope.type === envelopeTypeEnum.PLAINTEXT_CONTENT) {
|
||||
const buffer = Buffer.from(ciphertext.toArrayBuffer());
|
||||
const plaintextContent = PlaintextContent.deserialize(buffer);
|
||||
|
||||
promise = Promise.resolve(
|
||||
this.unpad(typedArrayToArrayBuffer(plaintextContent.body()))
|
||||
);
|
||||
} else if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
|
||||
window.log.info('message from', this.getEnvelopeId(envelope));
|
||||
if (!identifier) {
|
||||
throw new Error(
|
||||
|
@ -1215,6 +1248,13 @@ class MessageReceiverInner extends EventTarget {
|
|||
originalSource || originalSourceUuid
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
envelope.contentHint = messageContent.contentHint();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
envelope.groupId = messageContent.groupId()?.toString('base64');
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
envelope.usmc = messageContent;
|
||||
|
||||
if (
|
||||
(envelope.source && this.isBlocked(envelope.source)) ||
|
||||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid))
|
||||
|
@ -1231,6 +1271,17 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
if (
|
||||
messageContent.msgType() ===
|
||||
unidentifiedSenderTypeEnum.PLAINTEXT_CONTENT
|
||||
) {
|
||||
const plaintextContent = PlaintextContent.deserialize(
|
||||
messageContent.contents()
|
||||
);
|
||||
|
||||
return plaintextContent.body();
|
||||
}
|
||||
|
||||
if (
|
||||
messageContent.msgType() ===
|
||||
unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE
|
||||
|
@ -1345,10 +1396,26 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
|
||||
if (uuid && deviceId) {
|
||||
// It is safe (from deadlocks) to await this call because the session
|
||||
// reset is going to be scheduled on a separate p-queue in
|
||||
// ts/background.ts
|
||||
await this.lightSessionReset(uuid, deviceId);
|
||||
const event = new Event('decryption-error');
|
||||
event.decryptionError = {
|
||||
cipherTextBytes: envelope.usmc
|
||||
? typedArrayToArrayBuffer(envelope.usmc.contents())
|
||||
: undefined,
|
||||
cipherTextType: envelope.usmc ? envelope.usmc.msgType() : undefined,
|
||||
contentHint: envelope.contentHint,
|
||||
groupId: envelope.groupId,
|
||||
receivedAtCounter: envelope.receivedAtCounter,
|
||||
receivedAtDate: envelope.receivedAtDate,
|
||||
senderDevice: deviceId,
|
||||
senderUuid: uuid,
|
||||
timestamp: envelope.timestamp.toNumber(),
|
||||
};
|
||||
|
||||
// Avoid deadlocks by scheduling processing on decrypted queue
|
||||
this.addToQueue(
|
||||
() => this.dispatchAndWait(event),
|
||||
TaskType.Decrypted
|
||||
);
|
||||
} else {
|
||||
const envelopeId = this.getEnvelopeId(envelope);
|
||||
window.log.error(
|
||||
|
@ -1360,40 +1427,6 @@ class MessageReceiverInner extends EventTarget {
|
|||
});
|
||||
}
|
||||
|
||||
isOverHourIntoPast(timestamp: number): boolean {
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const now = Date.now();
|
||||
const oneHourIntoPast = now - HOUR;
|
||||
|
||||
return isNumber(timestamp) && timestamp <= oneHourIntoPast;
|
||||
}
|
||||
|
||||
// We don't lose anything if we delete keys over an hour into the past, because we only
|
||||
// change our behavior if the timestamps stored are less than an hour ago.
|
||||
cleanupSessionResets(): void {
|
||||
const sessionResets = window.storage.get(
|
||||
'sessionResets',
|
||||
{}
|
||||
) as SessionResetsType;
|
||||
|
||||
const keys = Object.keys(sessionResets);
|
||||
keys.forEach(key => {
|
||||
const timestamp = sessionResets[key];
|
||||
if (!timestamp || this.isOverHourIntoPast(timestamp)) {
|
||||
delete sessionResets[key];
|
||||
}
|
||||
});
|
||||
|
||||
window.storage.put('sessionResets', sessionResets);
|
||||
}
|
||||
|
||||
async lightSessionReset(uuid: string, deviceId: number): Promise<void> {
|
||||
const event = new Event('light-session-reset');
|
||||
event.senderUuid = uuid;
|
||||
event.senderDevice = deviceId;
|
||||
await this.dispatchAndWait(event);
|
||||
}
|
||||
|
||||
async handleSentMessage(
|
||||
envelope: EnvelopeClass,
|
||||
sentContainer: SyncMessageClass.Sent
|
||||
|
@ -1630,7 +1663,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
// make sure to process it first. If that fails, we still try to process
|
||||
// the rest of the message.
|
||||
try {
|
||||
if (content.senderKeyDistributionMessage) {
|
||||
if (
|
||||
content.senderKeyDistributionMessage &&
|
||||
!isByteBufferEmpty(content.senderKeyDistributionMessage)
|
||||
) {
|
||||
await this.handleSenderKeyDistributionMessage(
|
||||
envelope,
|
||||
content.senderKeyDistributionMessage
|
||||
|
@ -1643,6 +1679,16 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
if (
|
||||
content.decryptionErrorMessage &&
|
||||
!isByteBufferEmpty(content.decryptionErrorMessage)
|
||||
) {
|
||||
await this.handleDecryptionError(
|
||||
envelope,
|
||||
content.decryptionErrorMessage
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (content.syncMessage) {
|
||||
await this.handleSyncMessage(envelope, content.syncMessage);
|
||||
return;
|
||||
|
@ -1675,6 +1721,34 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
async handleDecryptionError(
|
||||
envelope: EnvelopeClass,
|
||||
decryptionError: ByteBufferClass
|
||||
) {
|
||||
const envelopeId = this.getEnvelopeId(envelope);
|
||||
window.log.info(`handleDecryptionError: ${envelopeId}`);
|
||||
|
||||
const buffer = Buffer.from(decryptionError.toArrayBuffer());
|
||||
const request = DecryptionErrorMessage.deserialize(buffer);
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
|
||||
const { sourceUuid, sourceDevice } = envelope;
|
||||
if (!sourceUuid || !sourceDevice) {
|
||||
window.log.error('handleDecryptionError: Missing uuid or device!');
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new Event('retry-request');
|
||||
event.retryRequest = {
|
||||
sentAt: request.timestamp(),
|
||||
requesterUuid: sourceUuid,
|
||||
requesterDevice: sourceDevice,
|
||||
senderDevice: request.deviceId(),
|
||||
};
|
||||
await this.dispatchAndWait(event);
|
||||
}
|
||||
|
||||
async handleSenderKeyDistributionMessage(
|
||||
envelope: EnvelopeClass,
|
||||
distributionMessage: ByteBufferClass
|
||||
|
@ -2603,10 +2677,6 @@ export default class MessageReceiver {
|
|||
this.stopProcessing = inner.stopProcessing.bind(inner);
|
||||
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
|
||||
|
||||
// For tests
|
||||
this.isOverHourIntoPast = inner.isOverHourIntoPast.bind(inner);
|
||||
this.cleanupSessionResets = inner.cleanupSessionResets.bind(inner);
|
||||
|
||||
inner.connect();
|
||||
this.getProcessedCount = () => inner.processedCount;
|
||||
}
|
||||
|
@ -2629,10 +2699,6 @@ export default class MessageReceiver {
|
|||
|
||||
unregisterBatchers: () => void;
|
||||
|
||||
isOverHourIntoPast: (timestamp: number) => boolean;
|
||||
|
||||
cleanupSessionResets: () => void;
|
||||
|
||||
getProcessedCount: () => number;
|
||||
|
||||
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
|
||||
|
|
|
@ -13,10 +13,13 @@ import { reject } from 'lodash';
|
|||
import { z } from 'zod';
|
||||
import {
|
||||
CiphertextMessageType,
|
||||
CiphertextMessage,
|
||||
PlaintextContent,
|
||||
ProtocolAddress,
|
||||
sealedSenderEncryptMessage,
|
||||
sealedSenderEncrypt,
|
||||
SenderCertificate,
|
||||
signalEncrypt,
|
||||
UnidentifiedSenderMessageContent,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { WebAPIType } from './WebAPI';
|
||||
|
@ -73,6 +76,9 @@ function ciphertextMessageTypeToEnvelopeType(type: number) {
|
|||
if (type === CiphertextMessageType.Whisper) {
|
||||
return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT;
|
||||
}
|
||||
if (type === CiphertextMessageType.Plaintext) {
|
||||
return window.textsecure.protobuf.Envelope.Type.PLAINTEXT_CONTENT;
|
||||
}
|
||||
throw new Error(
|
||||
`ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}`
|
||||
);
|
||||
|
@ -106,12 +112,10 @@ export default class OutgoingMessage {
|
|||
|
||||
identifiers: Array<string>;
|
||||
|
||||
message: ContentClass;
|
||||
message: ContentClass | PlaintextContent;
|
||||
|
||||
callback: (result: CallbackResultType) => void;
|
||||
|
||||
silent?: boolean;
|
||||
|
||||
plaintext?: Uint8Array;
|
||||
|
||||
identifiersCompleted: number;
|
||||
|
@ -128,12 +132,17 @@ export default class OutgoingMessage {
|
|||
|
||||
online?: boolean;
|
||||
|
||||
groupId?: string;
|
||||
|
||||
contentHint: number;
|
||||
|
||||
constructor(
|
||||
server: WebAPIType,
|
||||
timestamp: number,
|
||||
identifiers: Array<string>,
|
||||
message: ContentClass | DataMessageClass,
|
||||
silent: boolean | undefined,
|
||||
message: ContentClass | DataMessageClass | PlaintextContent,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
callback: (result: CallbackResultType) => void,
|
||||
options: OutgoingMessageOptionsType = {}
|
||||
) {
|
||||
|
@ -149,8 +158,9 @@ export default class OutgoingMessage {
|
|||
this.server = server;
|
||||
this.timestamp = timestamp;
|
||||
this.identifiers = identifiers;
|
||||
this.contentHint = contentHint;
|
||||
this.groupId = groupId;
|
||||
this.callback = callback;
|
||||
this.silent = silent;
|
||||
|
||||
this.identifiersCompleted = 0;
|
||||
this.errors = [];
|
||||
|
@ -186,12 +196,7 @@ export default class OutgoingMessage {
|
|||
if (error && error.code === 428) {
|
||||
error = new SendMessageChallengeError(identifier, error);
|
||||
} else {
|
||||
error = new OutgoingMessageError(
|
||||
identifier,
|
||||
this.message.toArrayBuffer(),
|
||||
this.timestamp,
|
||||
error
|
||||
);
|
||||
error = new OutgoingMessageError(identifier, null, null, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,7 +251,6 @@ export default class OutgoingMessage {
|
|||
} catch (error) {
|
||||
if (error?.message?.includes('untrusted identity for address')) {
|
||||
error.timestamp = this.timestamp;
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
@ -265,7 +269,6 @@ export default class OutgoingMessage {
|
|||
identifier,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
this.online,
|
||||
{ accessKey }
|
||||
);
|
||||
|
@ -274,7 +277,6 @@ export default class OutgoingMessage {
|
|||
identifier,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
this.online
|
||||
);
|
||||
}
|
||||
|
@ -299,18 +301,45 @@ export default class OutgoingMessage {
|
|||
|
||||
getPlaintext(): ArrayBuffer {
|
||||
if (!this.plaintext) {
|
||||
this.plaintext = padMessage(this.message.toArrayBuffer());
|
||||
const { message } = this;
|
||||
|
||||
if (message instanceof window.textsecure.protobuf.Content) {
|
||||
this.plaintext = padMessage(message.toArrayBuffer());
|
||||
} else {
|
||||
this.plaintext = message.serialize();
|
||||
}
|
||||
}
|
||||
return this.plaintext;
|
||||
}
|
||||
|
||||
async getCiphertextMessage({
|
||||
identityKeyStore,
|
||||
protocolAddress,
|
||||
sessionStore,
|
||||
}: {
|
||||
identityKeyStore: IdentityKeys;
|
||||
protocolAddress: ProtocolAddress;
|
||||
sessionStore: Sessions;
|
||||
}): Promise<CiphertextMessage> {
|
||||
const { message } = this;
|
||||
|
||||
if (message instanceof window.textsecure.protobuf.Content) {
|
||||
return signalEncrypt(
|
||||
Buffer.from(this.getPlaintext()),
|
||||
protocolAddress,
|
||||
sessionStore,
|
||||
identityKeyStore
|
||||
);
|
||||
}
|
||||
|
||||
return message.asCiphertextMessage();
|
||||
}
|
||||
|
||||
async doSendMessage(
|
||||
identifier: string,
|
||||
deviceIds: Array<number>,
|
||||
recurse?: boolean
|
||||
): Promise<void> {
|
||||
const plaintext = this.getPlaintext();
|
||||
|
||||
const { sendMetadata } = this;
|
||||
const { accessKey, senderCertificate } = sendMetadata?.[identifier] || {};
|
||||
|
||||
|
@ -364,15 +393,29 @@ export default class OutgoingMessage {
|
|||
const destinationRegistrationId = activeSession.remoteRegistrationId();
|
||||
|
||||
if (sealedSender && senderCertificate) {
|
||||
const ciphertextMessage = await this.getCiphertextMessage({
|
||||
identityKeyStore,
|
||||
protocolAddress,
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
const certificate = SenderCertificate.deserialize(
|
||||
Buffer.from(senderCertificate.serialized)
|
||||
);
|
||||
const groupIdBuffer = this.groupId
|
||||
? Buffer.from(this.groupId, 'base64')
|
||||
: null;
|
||||
|
||||
const buffer = await sealedSenderEncryptMessage(
|
||||
Buffer.from(plaintext),
|
||||
protocolAddress,
|
||||
const content = UnidentifiedSenderMessageContent.new(
|
||||
ciphertextMessage,
|
||||
certificate,
|
||||
sessionStore,
|
||||
this.contentHint,
|
||||
groupIdBuffer
|
||||
);
|
||||
|
||||
const buffer = await sealedSenderEncrypt(
|
||||
content,
|
||||
protocolAddress,
|
||||
identityKeyStore
|
||||
);
|
||||
|
||||
|
@ -385,12 +428,11 @@ export default class OutgoingMessage {
|
|||
};
|
||||
}
|
||||
|
||||
const ciphertextMessage = await signalEncrypt(
|
||||
Buffer.from(plaintext),
|
||||
const ciphertextMessage = await this.getCiphertextMessage({
|
||||
identityKeyStore,
|
||||
protocolAddress,
|
||||
sessionStore,
|
||||
identityKeyStore
|
||||
);
|
||||
});
|
||||
const type = ciphertextMessageTypeToEnvelopeType(
|
||||
ciphertextMessage.type()
|
||||
);
|
||||
|
@ -487,8 +529,6 @@ export default class OutgoingMessage {
|
|||
if (error?.message?.includes('untrusted identity for address')) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.timestamp = this.timestamp;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
window.log.error(
|
||||
'Got "key changed" error from encrypt - no identityKey for application layer',
|
||||
identifier,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Dictionary } from 'lodash';
|
|||
import PQueue from 'p-queue';
|
||||
import { AbortSignal } from 'abort-controller';
|
||||
import {
|
||||
PlaintextContent,
|
||||
ProtocolAddress,
|
||||
SenderKeyDistributionMessage,
|
||||
} from '@signalapp/signal-client';
|
||||
|
@ -795,10 +796,11 @@ export default class MessageSender {
|
|||
|
||||
async sendMessage(
|
||||
attrs: MessageOptionsType,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const message = new Message(attrs);
|
||||
const silent = false;
|
||||
|
||||
return Promise.all([
|
||||
this.uploadAttachments(message),
|
||||
|
@ -812,6 +814,8 @@ export default class MessageSender {
|
|||
message.timestamp,
|
||||
message.recipients || [],
|
||||
message.toProto(),
|
||||
contentHint,
|
||||
groupId,
|
||||
(res: CallbackResultType) => {
|
||||
res.dataMessage = message.toArrayBuffer();
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
|
@ -820,7 +824,6 @@ export default class MessageSender {
|
|||
resolve(res);
|
||||
}
|
||||
},
|
||||
silent,
|
||||
options
|
||||
);
|
||||
})
|
||||
|
@ -830,9 +833,10 @@ export default class MessageSender {
|
|||
sendMessageProto(
|
||||
timestamp: number,
|
||||
recipients: Array<string>,
|
||||
messageProto: ContentClass | DataMessageClass,
|
||||
messageProto: ContentClass | DataMessageClass | PlaintextContent,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
callback: (result: CallbackResultType) => void,
|
||||
silent?: boolean,
|
||||
options?: SendOptionsType
|
||||
): void {
|
||||
const rejections = window.textsecure.storage.get(
|
||||
|
@ -848,7 +852,8 @@ export default class MessageSender {
|
|||
timestamp,
|
||||
recipients,
|
||||
messageProto,
|
||||
silent,
|
||||
contentHint,
|
||||
groupId,
|
||||
callback,
|
||||
options
|
||||
);
|
||||
|
@ -863,8 +868,9 @@ export default class MessageSender {
|
|||
async sendMessageProtoAndWait(
|
||||
timestamp: number,
|
||||
identifiers: Array<string>,
|
||||
messageProto: DataMessageClass,
|
||||
silent?: boolean,
|
||||
messageProto: ContentClass | DataMessageClass | PlaintextContent,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -881,8 +887,9 @@ export default class MessageSender {
|
|||
timestamp,
|
||||
identifiers,
|
||||
messageProto,
|
||||
contentHint,
|
||||
groupId,
|
||||
callback,
|
||||
silent,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
@ -890,9 +897,9 @@ export default class MessageSender {
|
|||
|
||||
async sendIndividualProto(
|
||||
identifier: string,
|
||||
proto: DataMessageClass | ContentClass,
|
||||
proto: DataMessageClass | ContentClass | PlaintextContent,
|
||||
timestamp: number,
|
||||
silent?: boolean,
|
||||
contentHint: number,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -907,13 +914,16 @@ export default class MessageSender {
|
|||
timestamp,
|
||||
[identifier],
|
||||
proto,
|
||||
contentHint,
|
||||
undefined, // groupId
|
||||
callback,
|
||||
silent,
|
||||
options
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// You might wonder why this takes a groupId. models/messages.resend() can send a group
|
||||
// message to just one person.
|
||||
async sendMessageToIdentifier(
|
||||
identifier: string,
|
||||
messageText: string | undefined,
|
||||
|
@ -925,6 +935,8 @@ export default class MessageSender {
|
|||
deletedForEveryoneTimestamp: number | undefined,
|
||||
timestamp: number,
|
||||
expireTimer: number | undefined,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
|
@ -942,6 +954,8 @@ export default class MessageSender {
|
|||
expireTimer,
|
||||
profileKey,
|
||||
},
|
||||
contentHint,
|
||||
groupId,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1018,12 +1032,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
timestamp,
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1043,12 +1060,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1071,12 +1091,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1098,12 +1121,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1127,12 +1153,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1160,12 +1189,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1189,12 +1221,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1224,12 +1259,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1261,12 +1299,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1299,12 +1340,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
@ -1344,12 +1388,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1397,12 +1444,15 @@ export default class MessageSender {
|
|||
const secondMessage = new window.textsecure.protobuf.Content();
|
||||
secondMessage.syncMessage = syncMessage;
|
||||
|
||||
const innerSilent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
secondMessage,
|
||||
now,
|
||||
innerSilent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
@ -1416,6 +1466,10 @@ export default class MessageSender {
|
|||
sendOptions: SendOptionsType,
|
||||
groupId?: string
|
||||
): Promise<CallbackResultType> {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients,
|
||||
|
@ -1431,6 +1485,8 @@ export default class MessageSender {
|
|||
}
|
||||
: {}),
|
||||
},
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // groupId
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
@ -1446,13 +1502,16 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.callingMessage = callingMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendMessageProtoAndWait(
|
||||
finalTimestamp,
|
||||
recipients,
|
||||
contentMessage,
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // groupId
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
@ -1481,12 +1540,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.receiptMessage = receiptMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
recipientUuid || recipientE164,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1504,12 +1566,15 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.receiptMessage = receiptMessage;
|
||||
|
||||
const silent = true;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto(
|
||||
senderUuid || senderE164,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1534,14 +1599,17 @@ export default class MessageSender {
|
|||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.nullMessage = nullMessage;
|
||||
|
||||
// We want the NullMessage to look like a normal outgoing message; not silent
|
||||
const silent = false;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
// We want the NullMessage to look like a normal outgoing message
|
||||
const timestamp = Date.now();
|
||||
return this.sendIndividualProto(
|
||||
identifier,
|
||||
contentMessage,
|
||||
timestamp,
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1555,7 +1623,6 @@ export default class MessageSender {
|
|||
CallbackResultType | void | Array<CallbackResultType | void | Array<void>>
|
||||
> {
|
||||
window.log.info('resetSession: start');
|
||||
const silent = false;
|
||||
const proto = new window.textsecure.protobuf.DataMessage();
|
||||
proto.body = 'TERMINATE';
|
||||
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
|
@ -1568,6 +1635,10 @@ export default class MessageSender {
|
|||
throw error;
|
||||
};
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const sendToContactPromise = window.textsecure.storage.protocol
|
||||
.archiveAllSessions(identifier)
|
||||
.catch(logError('resetSession/archiveAllSessions1 error:'))
|
||||
|
@ -1579,7 +1650,7 @@ export default class MessageSender {
|
|||
identifier,
|
||||
proto,
|
||||
timestamp,
|
||||
silent,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
options
|
||||
).catch(logError('resetSession/sendToContact error:'));
|
||||
})
|
||||
|
@ -1619,6 +1690,10 @@ export default class MessageSender {
|
|||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients: [identifier],
|
||||
|
@ -1628,6 +1703,31 @@ export default class MessageSender {
|
|||
flags:
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
},
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // groupId
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async sendRetryRequest({
|
||||
options,
|
||||
plaintext,
|
||||
uuid,
|
||||
}: {
|
||||
options?: SendOptionsType;
|
||||
plaintext: PlaintextContent;
|
||||
uuid: string;
|
||||
}): Promise<CallbackResultType> {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendMessageProtoAndWait(
|
||||
Date.now(),
|
||||
[uuid],
|
||||
plaintext,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // groupId
|
||||
options
|
||||
);
|
||||
}
|
||||
|
@ -1639,6 +1739,8 @@ export default class MessageSender {
|
|||
providedIdentifiers: Array<string>,
|
||||
proto: ContentClass,
|
||||
timestamp = Date.now(),
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const myE164 = window.textsecure.storage.user.getNumber();
|
||||
|
@ -1658,7 +1760,6 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const silent = true;
|
||||
const callback = (res: CallbackResultType) => {
|
||||
res.dataMessage = proto.dataMessage?.toArrayBuffer();
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
|
@ -1672,21 +1773,17 @@ export default class MessageSender {
|
|||
timestamp,
|
||||
providedIdentifiers,
|
||||
proto,
|
||||
contentHint,
|
||||
groupId,
|
||||
callback,
|
||||
silent,
|
||||
options
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// The one group send exception - a message that should never be sent via sender key
|
||||
async sendSenderKeyDistributionMessage(
|
||||
{
|
||||
distributionId,
|
||||
identifiers,
|
||||
}: { distributionId: string; identifiers: Array<string> },
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
async getSenderKeyDistributionMessage(
|
||||
distributionId: string
|
||||
): Promise<SenderKeyDistributionMessage> {
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
if (!ourUuid) {
|
||||
throw new Error(
|
||||
|
@ -1702,7 +1799,7 @@ export default class MessageSender {
|
|||
const address = `${ourUuid}.${ourDeviceId}`;
|
||||
const senderKeyStore = new SenderKeys();
|
||||
|
||||
const message = await window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||
return window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||
address,
|
||||
async () =>
|
||||
SenderKeyDistributionMessage.create(
|
||||
|
@ -1711,13 +1808,40 @@ export default class MessageSender {
|
|||
senderKeyStore
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const proto = new window.textsecure.protobuf.Content();
|
||||
proto.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap(
|
||||
typedArrayToArrayBuffer(message.serialize())
|
||||
// The one group send exception - a message that should never be sent via sender key
|
||||
async sendSenderKeyDistributionMessage(
|
||||
{
|
||||
contentHint,
|
||||
distributionId,
|
||||
groupId,
|
||||
identifiers,
|
||||
}: {
|
||||
contentHint: number;
|
||||
distributionId: string;
|
||||
groupId: string | undefined;
|
||||
identifiers: Array<string>;
|
||||
},
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
|
||||
const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage(
|
||||
distributionId
|
||||
);
|
||||
contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap(
|
||||
typedArrayToArrayBuffer(senderKeyDistributionMessage.serialize())
|
||||
);
|
||||
|
||||
return this.sendGroupProto(identifiers, proto, Date.now(), options);
|
||||
return this.sendGroupProto(
|
||||
identifiers,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
contentHint,
|
||||
groupId,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
// GroupV1-only functions; not to be used in the future
|
||||
|
@ -1731,7 +1855,18 @@ export default class MessageSender {
|
|||
proto.group = new window.textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(groupId);
|
||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.QUIT;
|
||||
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
return this.sendGroupProto(
|
||||
groupIdentifiers,
|
||||
proto,
|
||||
Date.now(),
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // only for GV2 ids
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async sendExpirationTimerUpdateToGroup(
|
||||
|
@ -1770,7 +1905,15 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
return this.sendMessage(attrs, options);
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
return this.sendMessage(
|
||||
attrs,
|
||||
ContentHint.SUPPLEMENTARY,
|
||||
undefined, // only for GV2 ids
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
// Simple pass-throughs
|
||||
|
|
|
@ -934,14 +934,12 @@ export type WebAPIType = {
|
|||
destination: string,
|
||||
messageArray: Array<MessageType>,
|
||||
timestamp: number,
|
||||
silent?: boolean,
|
||||
online?: boolean
|
||||
) => Promise<void>;
|
||||
sendMessagesUnauth: (
|
||||
destination: string,
|
||||
messageArray: Array<MessageType>,
|
||||
timestamp: number,
|
||||
silent?: boolean,
|
||||
online?: boolean,
|
||||
options?: { accessKey?: string }
|
||||
) => Promise<void>;
|
||||
|
@ -1446,7 +1444,7 @@ export function initialize({
|
|||
const capabilities: CapabilitiesUploadType = {
|
||||
'gv2-3': true,
|
||||
'gv1-migration': true,
|
||||
senderKey: false,
|
||||
senderKey: true,
|
||||
};
|
||||
|
||||
const { accessKey } = options;
|
||||
|
@ -1684,15 +1682,11 @@ export function initialize({
|
|||
destination: string,
|
||||
messageArray: Array<MessageType>,
|
||||
timestamp: number,
|
||||
silent?: boolean,
|
||||
online?: boolean,
|
||||
{ accessKey }: { accessKey?: string } = {}
|
||||
) {
|
||||
const jsonData: any = { messages: messageArray, timestamp };
|
||||
|
||||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
if (online) {
|
||||
jsonData.online = true;
|
||||
}
|
||||
|
@ -1712,14 +1706,10 @@ export function initialize({
|
|||
destination: string,
|
||||
messageArray: Array<MessageType>,
|
||||
timestamp: number,
|
||||
silent?: boolean,
|
||||
online?: boolean
|
||||
) {
|
||||
const jsonData: any = { messages: messageArray, timestamp };
|
||||
|
||||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
if (online) {
|
||||
jsonData.online = true;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import * as zkgroup from './zkgroup';
|
|||
import { StartupQueue } from './StartupQueue';
|
||||
import { postLinkExperience } from './postLinkExperience';
|
||||
import { sendToGroup, sendContentMessageToGroup } from './sendToGroup';
|
||||
import { RetryPlaceholders } from './retryPlaceholders';
|
||||
|
||||
export {
|
||||
GoogleChrome,
|
||||
|
@ -62,6 +63,7 @@ export {
|
|||
parseRemoteClientExpiration,
|
||||
postLinkExperience,
|
||||
queueUpdateMessage,
|
||||
RetryPlaceholders,
|
||||
saveNewMessageBatcher,
|
||||
sendContentMessageToGroup,
|
||||
sendToGroup,
|
||||
|
|
196
ts/util/retryPlaceholders.ts
Normal file
196
ts/util/retryPlaceholders.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { z } from 'zod';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
const retryItemSchema = z
|
||||
.object({
|
||||
conversationId: z.string(),
|
||||
sentAt: z.number(),
|
||||
receivedAt: z.number(),
|
||||
receivedAtCounter: z.number(),
|
||||
senderUuid: z.string(),
|
||||
})
|
||||
.passthrough();
|
||||
export type RetryItemType = z.infer<typeof retryItemSchema>;
|
||||
|
||||
const retryItemListSchema = z.array(retryItemSchema);
|
||||
export type RetryItemListType = z.infer<typeof retryItemListSchema>;
|
||||
|
||||
export type ByConversationLookupType = {
|
||||
[key: string]: Array<RetryItemType>;
|
||||
};
|
||||
export type ByMessageLookupType = Map<string, RetryItemType>;
|
||||
|
||||
export function getItemId(conversationId: string, sentAt: number): string {
|
||||
return `${conversationId}--${sentAt}`;
|
||||
}
|
||||
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
export const STORAGE_KEY = 'retryPlaceholders';
|
||||
|
||||
export function getOneHourAgo(): number {
|
||||
return Date.now() - HOUR;
|
||||
}
|
||||
|
||||
export class RetryPlaceholders {
|
||||
private items: Array<RetryItemType>;
|
||||
|
||||
private byConversation: ByConversationLookupType;
|
||||
|
||||
private byMessage: ByMessageLookupType;
|
||||
|
||||
constructor() {
|
||||
if (!window.storage) {
|
||||
throw new Error(
|
||||
'RetryPlaceholders.constructor: window.storage not available!'
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = retryItemListSchema.safeParse(
|
||||
window.storage.get(STORAGE_KEY) || []
|
||||
);
|
||||
if (!parsed.success) {
|
||||
window.log.warn(
|
||||
`RetryPlaceholders.constructor: Data fetched from storage did not match schema: ${JSON.stringify(
|
||||
parsed.error.flatten()
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
this.items = parsed.success ? parsed.data : [];
|
||||
window.log.info(
|
||||
`RetryPlaceholders.constructor: Started with ${this.items.length} items`
|
||||
);
|
||||
|
||||
this.sortByExpiresAtAsc();
|
||||
this.byConversation = this.makeByConversationLookup();
|
||||
this.byMessage = this.makeByMessageLookup();
|
||||
}
|
||||
|
||||
// Arranging local data for efficiency
|
||||
|
||||
sortByExpiresAtAsc(): void {
|
||||
this.items.sort(
|
||||
(left: RetryItemType, right: RetryItemType) =>
|
||||
left.receivedAt - right.receivedAt
|
||||
);
|
||||
}
|
||||
|
||||
makeByConversationLookup(): ByConversationLookupType {
|
||||
return groupBy(this.items, item => item.conversationId);
|
||||
}
|
||||
|
||||
makeByMessageLookup(): ByMessageLookupType {
|
||||
const lookup = new Map<string, RetryItemType>();
|
||||
this.items.forEach(item => {
|
||||
lookup.set(getItemId(item.conversationId, item.sentAt), item);
|
||||
});
|
||||
return lookup;
|
||||
}
|
||||
|
||||
makeLookups(): void {
|
||||
this.byConversation = this.makeByConversationLookup();
|
||||
this.byMessage = this.makeByMessageLookup();
|
||||
}
|
||||
|
||||
// Basic data management
|
||||
|
||||
async add(item: RetryItemType): Promise<void> {
|
||||
const parsed = retryItemSchema.safeParse(item);
|
||||
if (!parsed.success) {
|
||||
throw new Error(
|
||||
`RetryPlaceholders.add: Item did not match schema ${JSON.stringify(
|
||||
parsed.error.flatten()
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
this.items.push(item);
|
||||
this.sortByExpiresAtAsc();
|
||||
this.makeLookups();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await window.storage.put(STORAGE_KEY, this.items);
|
||||
}
|
||||
|
||||
// Finding items in different ways
|
||||
|
||||
getCount(): number {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
getNextToExpire(): RetryItemType | undefined {
|
||||
return this.items[0];
|
||||
}
|
||||
|
||||
async getExpiredAndRemove(): Promise<Array<RetryItemType>> {
|
||||
const expiration = getOneHourAgo();
|
||||
const max = this.items.length;
|
||||
const result: Array<RetryItemType> = [];
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const item = this.items[i];
|
||||
if (item.receivedAt <= expiration) {
|
||||
result.push(item);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
`RetryPlaceholders.getExpiredAndRemove: Found ${result.length} expired items`
|
||||
);
|
||||
|
||||
this.items.splice(0, result.length);
|
||||
this.makeLookups();
|
||||
await this.save();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async findByConversationAndRemove(
|
||||
conversationId: string
|
||||
): Promise<Array<RetryItemType>> {
|
||||
const result = this.byConversation[conversationId];
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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(
|
||||
conversationId: string,
|
||||
sentAt: number
|
||||
): Promise<RetryItemType | undefined> {
|
||||
const result = this.byMessage.get(getItemId(conversationId, sentAt));
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const index = this.items.findIndex(item => item === result);
|
||||
|
||||
this.items.splice(index, 1);
|
||||
this.makeLookups();
|
||||
await this.save();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -55,6 +55,7 @@ const MAX_RECURSION = 5;
|
|||
export async function sendToGroup(
|
||||
groupSendOptions: GroupSendOptionsType,
|
||||
conversation: ConversationModel,
|
||||
contentHint: number,
|
||||
sendOptions?: SendOptionsType,
|
||||
isPartialSend?: boolean
|
||||
): Promise<CallbackResultType> {
|
||||
|
@ -75,6 +76,7 @@ export async function sendToGroup(
|
|||
);
|
||||
|
||||
return sendContentMessageToGroup({
|
||||
contentHint,
|
||||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
|
@ -85,6 +87,7 @@ export async function sendToGroup(
|
|||
}
|
||||
|
||||
export async function sendContentMessageToGroup({
|
||||
contentHint,
|
||||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
|
@ -93,6 +96,7 @@ export async function sendContentMessageToGroup({
|
|||
sendOptions,
|
||||
timestamp,
|
||||
}: {
|
||||
contentHint: number;
|
||||
contentMessage: ContentClass;
|
||||
conversation: ConversationModel;
|
||||
isPartialSend?: boolean;
|
||||
|
@ -110,6 +114,7 @@ export async function sendContentMessageToGroup({
|
|||
if (conversation.isGroupV2()) {
|
||||
try {
|
||||
return await sendToGroupViaSenderKey({
|
||||
contentHint,
|
||||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
|
@ -127,10 +132,15 @@ export async function sendContentMessageToGroup({
|
|||
}
|
||||
}
|
||||
|
||||
const groupId = conversation.isGroupV2()
|
||||
? conversation.get('groupId')
|
||||
: undefined;
|
||||
return window.textsecure.messaging.sendGroupProto(
|
||||
recipients,
|
||||
contentMessage,
|
||||
timestamp,
|
||||
contentHint,
|
||||
groupId,
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
@ -138,6 +148,7 @@ export async function sendContentMessageToGroup({
|
|||
// The Primary Sender Key workflow
|
||||
|
||||
export async function sendToGroupViaSenderKey(options: {
|
||||
contentHint: number;
|
||||
contentMessage: ContentClass;
|
||||
conversation: ConversationModel;
|
||||
isPartialSend?: boolean;
|
||||
|
@ -148,6 +159,7 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
timestamp: number;
|
||||
}): Promise<CallbackResultType> {
|
||||
const {
|
||||
contentHint,
|
||||
contentMessage,
|
||||
conversation,
|
||||
isPartialSend,
|
||||
|
@ -157,6 +169,9 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
sendOptions,
|
||||
timestamp,
|
||||
} = options;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const logId = conversation.idForLogging();
|
||||
window.log.info(
|
||||
|
@ -176,6 +191,15 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
);
|
||||
}
|
||||
|
||||
if (
|
||||
contentHint !== ContentHint.RESENDABLE &&
|
||||
contentHint !== ContentHint.SUPPLEMENTARY
|
||||
) {
|
||||
throw new Error(
|
||||
`sendToGroupViaSenderKey/${logId}: Invalid contentHint ${contentHint}`
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
window.textsecure.messaging,
|
||||
'sendToGroupViaSenderKey: textsecure.messaging not available!'
|
||||
|
@ -293,10 +317,15 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
newToMemberUuids.length
|
||||
} members: ${JSON.stringify(newToMemberUuids)}`
|
||||
);
|
||||
await window.textsecure.messaging.sendSenderKeyDistributionMessage({
|
||||
await window.textsecure.messaging.sendSenderKeyDistributionMessage(
|
||||
{
|
||||
contentHint: ContentHint.SUPPLEMENTARY,
|
||||
distributionId,
|
||||
groupId,
|
||||
identifiers: newToMemberUuids,
|
||||
});
|
||||
},
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
||||
// 9. Update memberDevices with both adds and the removals which didn't require a reset.
|
||||
|
@ -323,6 +352,7 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
// 10. Send the Sender Key message!
|
||||
try {
|
||||
const messageBuffer = await encryptForSenderKey({
|
||||
contentHint,
|
||||
devices: devicesForSenderKey,
|
||||
distributionId,
|
||||
contentMessage: contentMessage.toArrayBuffer(),
|
||||
|
@ -396,6 +426,8 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
normalRecipients,
|
||||
contentMessage,
|
||||
timestamp,
|
||||
contentHint,
|
||||
groupId,
|
||||
sendOptions
|
||||
);
|
||||
|
||||
|
@ -594,14 +626,16 @@ function getXorOfAccessKeys(devices: Array<DeviceType>): Buffer {
|
|||
}
|
||||
|
||||
async function encryptForSenderKey({
|
||||
contentHint,
|
||||
contentMessage,
|
||||
devices,
|
||||
distributionId,
|
||||
contentMessage,
|
||||
groupId,
|
||||
}: {
|
||||
contentHint: number;
|
||||
contentMessage: ArrayBuffer;
|
||||
devices: Array<DeviceType>;
|
||||
distributionId: string;
|
||||
contentMessage: ArrayBuffer;
|
||||
groupId: string;
|
||||
}): Promise<Buffer> {
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
|
@ -625,7 +659,6 @@ async function encryptForSenderKey({
|
|||
() => groupEncrypt(sender, distributionId, senderKeyStore, message)
|
||||
);
|
||||
|
||||
const contentHint = 1;
|
||||
const groupIdBuffer = Buffer.from(groupId, 'base64');
|
||||
const senderCertificateObject = await senderCertificateService.get(
|
||||
SenderCertificateMode.WithoutE164
|
||||
|
@ -676,8 +709,8 @@ function isValidSenderKeyRecipient(
|
|||
return false;
|
||||
}
|
||||
|
||||
const { capabilities } = memberConversation.attributes;
|
||||
if (!capabilities.senderKey) {
|
||||
const capabilities = memberConversation.get('capabilities');
|
||||
if (!capabilities?.senderKey) {
|
||||
window.log.info(
|
||||
`isValidSenderKeyRecipient: Missing senderKey capability for member ${uuid}`
|
||||
);
|
||||
|
|
|
@ -394,12 +394,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.model.throttledGetProfiles =
|
||||
this.model.throttledGetProfiles ||
|
||||
window._.throttle(this.model.getProfiles.bind(this.model), FIVE_MINUTES);
|
||||
this.model.throttledMaybeMigrateV1Group =
|
||||
this.model.throttledMaybeMigrateV1Group ||
|
||||
window._.throttle(
|
||||
this.model.maybeMigrateV1Group.bind(this.model),
|
||||
FIVE_MINUTES
|
||||
);
|
||||
|
||||
this.debouncedMaybeGrabLinkPreview = window._.debounce(
|
||||
this.maybeGrabLinkPreview.bind(this),
|
||||
|
@ -2171,6 +2165,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
|
||||
async onOpened(messageId: any) {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
|
||||
if (messageId) {
|
||||
const message = await getMessageById(messageId, {
|
||||
Message: Whisper.Message,
|
||||
|
@ -2184,29 +2180,41 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
window.log.warn(`onOpened: Did not find message ${messageId}`);
|
||||
}
|
||||
|
||||
const { retryPlaceholders } = window.Signal.Services;
|
||||
if (retryPlaceholders) {
|
||||
const placeholders = await retryPlaceholders.findByConversationAndRemove(
|
||||
model.id
|
||||
);
|
||||
window.log.info(`onOpened: Found ${placeholders.length} placeholders`);
|
||||
}
|
||||
|
||||
this.loadNewestMessages();
|
||||
this.model.updateLastMessage();
|
||||
model.updateLastMessage();
|
||||
|
||||
this.focusMessageField();
|
||||
|
||||
const quotedMessageId = this.model.get('quotedMessageId');
|
||||
const quotedMessageId = model.get('quotedMessageId');
|
||||
if (quotedMessageId) {
|
||||
this.setQuoteMessage(quotedMessageId);
|
||||
}
|
||||
|
||||
this.model.fetchLatestGroupV2Data();
|
||||
this.model.throttledMaybeMigrateV1Group();
|
||||
model.fetchLatestGroupV2Data();
|
||||
assert(
|
||||
this.model.throttledFetchSMSOnlyUUID !== undefined,
|
||||
model.throttledMaybeMigrateV1Group !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
this.model.throttledFetchSMSOnlyUUID();
|
||||
model.throttledMaybeMigrateV1Group();
|
||||
assert(
|
||||
model.throttledFetchSMSOnlyUUID !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
model.throttledFetchSMSOnlyUUID();
|
||||
|
||||
const statusPromise = this.model.throttledGetProfiles();
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.statusFetch = statusPromise.then(() =>
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.model.updateVerified().then(() => {
|
||||
model.updateVerified().then(() => {
|
||||
this.onVerifiedChange();
|
||||
this.statusFetch = null;
|
||||
})
|
||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -315,6 +315,7 @@ declare global {
|
|||
) => void;
|
||||
onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
|
||||
removeTimeout: (uuid: string) => void;
|
||||
retryPlaceholders?: Util.RetryPlaceholders;
|
||||
runStorageServiceSyncJob: () => Promise<void>;
|
||||
storageServiceUploadJob: () => void;
|
||||
};
|
||||
|
|
|
@ -1634,10 +1634,10 @@
|
|||
react-lifecycles-compat "^3.0.4"
|
||||
warning "^3.0.0"
|
||||
|
||||
"@signalapp/signal-client@0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.6.0.tgz#65b3affe66d73b63daf3494e027470b3d824674a"
|
||||
integrity sha512-EhuQeloFqtagd4QxfNsJjKLG0P2bQwv1tB9u5hqLWVsIL8wWUcMYSaPxFAXMbPpmLPu3u3378scr1w861lcHxg==
|
||||
"@signalapp/signal-client@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.8.0.tgz#30c3bfafbd32680c8dd7e5417e53b928b1ccdd65"
|
||||
integrity sha512-pchM+cwWdJZSCIceUvq/2lNZr6qJO7qGpQMfxbm9CGrcQaU7t7vtrkR5F0AsHnYO+lfL/3mMOVbBb0Rgl5/IVw==
|
||||
dependencies:
|
||||
node-gyp-build "^4.2.3"
|
||||
uuid "^8.3.0"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue