Additional work to include story=true on send
This commit is contained in:
parent
3bfeffe502
commit
4ec48df5b9
22 changed files with 327 additions and 170 deletions
|
@ -196,7 +196,7 @@
|
||||||
"@babel/preset-typescript": "7.17.12",
|
"@babel/preset-typescript": "7.17.12",
|
||||||
"@electron/fuses": "1.5.0",
|
"@electron/fuses": "1.5.0",
|
||||||
"@mixer/parallel-prettier": "2.0.1",
|
"@mixer/parallel-prettier": "2.0.1",
|
||||||
"@signalapp/mock-server": "2.10.0",
|
"@signalapp/mock-server": "2.11.0",
|
||||||
"@storybook/addon-a11y": "6.5.6",
|
"@storybook/addon-a11y": "6.5.6",
|
||||||
"@storybook/addon-actions": "6.5.6",
|
"@storybook/addon-actions": "6.5.6",
|
||||||
"@storybook/addon-controls": "6.5.6",
|
"@storybook/addon-controls": "6.5.6",
|
||||||
|
|
|
@ -35,7 +35,8 @@ message Envelope {
|
||||||
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
|
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
|
||||||
optional bool urgent = 14 [default=true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical
|
optional bool urgent = 14 [default=true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical
|
||||||
optional string updated_pni = 15;
|
optional string updated_pni = 15;
|
||||||
// next: 16
|
optional bool story = 16; // indicates that the content is a story.
|
||||||
|
// next: 17
|
||||||
}
|
}
|
||||||
|
|
||||||
message Content {
|
message Content {
|
||||||
|
|
|
@ -131,32 +131,33 @@ message AccountRecord {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
optional bytes profileKey = 1;
|
optional bytes profileKey = 1;
|
||||||
optional string givenName = 2;
|
optional string givenName = 2;
|
||||||
optional string familyName = 3;
|
optional string familyName = 3;
|
||||||
optional string avatarUrl = 4;
|
optional string avatarUrl = 4;
|
||||||
optional bool noteToSelfArchived = 5;
|
optional bool noteToSelfArchived = 5;
|
||||||
optional bool readReceipts = 6;
|
optional bool readReceipts = 6;
|
||||||
optional bool sealedSenderIndicators = 7;
|
optional bool sealedSenderIndicators = 7;
|
||||||
optional bool typingIndicators = 8;
|
optional bool typingIndicators = 8;
|
||||||
optional bool proxiedLinkPreviews = 9;
|
optional bool proxiedLinkPreviews = 9;
|
||||||
optional bool noteToSelfMarkedUnread = 10;
|
optional bool noteToSelfMarkedUnread = 10;
|
||||||
optional bool linkPreviews = 11;
|
optional bool linkPreviews = 11;
|
||||||
optional PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
optional PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
||||||
optional bool notDiscoverableByPhoneNumber = 13;
|
optional bool notDiscoverableByPhoneNumber = 13;
|
||||||
repeated PinnedConversation pinnedConversations = 14;
|
repeated PinnedConversation pinnedConversations = 14;
|
||||||
optional bool preferContactAvatars = 15;
|
optional bool preferContactAvatars = 15;
|
||||||
optional uint32 universalExpireTimer = 17;
|
optional uint32 universalExpireTimer = 17;
|
||||||
optional bool primarySendsSms = 18;
|
optional bool primarySendsSms = 18;
|
||||||
optional string e164 = 19;
|
optional string e164 = 19;
|
||||||
repeated string preferredReactionEmoji = 20;
|
repeated string preferredReactionEmoji = 20;
|
||||||
optional bytes subscriberId = 21;
|
optional bytes subscriberId = 21;
|
||||||
optional string subscriberCurrencyCode = 22;
|
optional string subscriberCurrencyCode = 22;
|
||||||
optional bool displayBadgesOnProfile = 23;
|
optional bool displayBadgesOnProfile = 23;
|
||||||
optional bool keepMutedChatsArchived = 25;
|
optional bool keepMutedChatsArchived = 25;
|
||||||
optional bool hasSetMyStoriesPrivacy = 26;
|
optional bool hasSetMyStoriesPrivacy = 26;
|
||||||
reserved /* hasViewedOnboardingStory */ 27;
|
reserved /* hasViewedOnboardingStory */ 27;
|
||||||
optional bool storiesDisabled = 28;
|
reserved 28; // deprecatedStoriesDisabled
|
||||||
|
optional bool storiesDisabled = 29;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StoryDistributionListRecord {
|
message StoryDistributionListRecord {
|
||||||
|
|
|
@ -183,6 +183,7 @@ export async function sendNormalMessage(
|
||||||
quote,
|
quote,
|
||||||
recipients: allRecipientIdentifiers,
|
recipients: allRecipientIdentifiers,
|
||||||
sticker,
|
sticker,
|
||||||
|
// No storyContext; you can't reply to your own stories
|
||||||
timestamp: messageTimestamp,
|
timestamp: messageTimestamp,
|
||||||
});
|
});
|
||||||
messageSendPromise = message.sendSyncMessageOnly(dataMessage, saveErrors);
|
messageSendPromise = message.sendSyncMessageOnly(dataMessage, saveErrors);
|
||||||
|
@ -234,6 +235,7 @@ export async function sendNormalMessage(
|
||||||
sendOptions,
|
sendOptions,
|
||||||
sendTarget: conversation.toSenderKeyTarget(),
|
sendTarget: conversation.toSenderKeyTarget(),
|
||||||
sendType: 'message',
|
sendType: 'message',
|
||||||
|
story: Boolean(storyContext),
|
||||||
urgent: true,
|
urgent: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -282,6 +284,7 @@ export async function sendNormalMessage(
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
timestamp: messageTimestamp,
|
timestamp: messageTimestamp,
|
||||||
|
// Note: 1:1 story replies should not set story=true - they aren't group sends
|
||||||
urgent: true,
|
urgent: true,
|
||||||
includePniSignatureMessage: true,
|
includePniSignatureMessage: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -264,7 +264,8 @@ export async function sendStory(
|
||||||
const recipientsSet = new Set(pendingSendRecipientIds);
|
const recipientsSet = new Set(pendingSendRecipientIds);
|
||||||
|
|
||||||
const sendOptions = await getSendOptionsForRecipients(
|
const sendOptions = await getSendOptionsForRecipients(
|
||||||
pendingSendRecipientIds
|
pendingSendRecipientIds,
|
||||||
|
{ story: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
@ -179,11 +179,13 @@ export class Reactions extends Collection<ReactionModel> {
|
||||||
storyReactionEmoji: reaction.get('emoji'),
|
storyReactionEmoji: reaction.get('emoji'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [generatedMessageId] = await Promise.all([
|
// Note: generatedMessage comes with an id, so we have to force this save
|
||||||
|
await Promise.all([
|
||||||
window.Signal.Data.saveMessage(generatedMessage.attributes, {
|
window.Signal.Data.saveMessage(generatedMessage.attributes, {
|
||||||
ourUuid: window.textsecure.storage.user
|
ourUuid: window.textsecure.storage.user
|
||||||
.getCheckedUuid()
|
.getCheckedUuid()
|
||||||
.toString(),
|
.toString(),
|
||||||
|
forceSave: true,
|
||||||
}),
|
}),
|
||||||
generatedMessage.hydrateStoryContext(message),
|
generatedMessage.hydrateStoryContext(message),
|
||||||
]);
|
]);
|
||||||
|
@ -197,10 +199,8 @@ export class Reactions extends Collection<ReactionModel> {
|
||||||
timestamp: reaction.get('timestamp'),
|
timestamp: reaction.get('timestamp'),
|
||||||
});
|
});
|
||||||
|
|
||||||
generatedMessage.set({ id: generatedMessageId });
|
|
||||||
|
|
||||||
const messageToAdd = window.MessageController.register(
|
const messageToAdd = window.MessageController.register(
|
||||||
generatedMessageId,
|
generatedMessage.id,
|
||||||
generatedMessage
|
generatedMessage
|
||||||
);
|
);
|
||||||
targetConversation.addSingleMessage(messageToAdd);
|
targetConversation.addSingleMessage(messageToAdd);
|
||||||
|
|
|
@ -159,7 +159,6 @@ import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
|
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
|
||||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
||||||
import { findStoryMessage } from '../util/findStoryMessage';
|
import { findStoryMessage } from '../util/findStoryMessage';
|
||||||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
|
||||||
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
|
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
|
||||||
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
||||||
import { getMessageById } from '../messages/getMessageById';
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
|
@ -2097,20 +2096,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
await conversation.queueJob('handleDataMessage', async () => {
|
await conversation.queueJob('handleDataMessage', async () => {
|
||||||
log.info(`${idLog}: starting processing in queue`);
|
log.info(`${idLog}: starting processing in queue`);
|
||||||
|
|
||||||
if (
|
|
||||||
isStory(message.attributes) &&
|
|
||||||
!isConversationAccepted(conversation.attributes, {
|
|
||||||
ignoreEmptyConvo: true,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
log.info(
|
|
||||||
`${idLog}: dropping story from !accepted`,
|
|
||||||
this.getSenderIdentifier()
|
|
||||||
);
|
|
||||||
confirm();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, check for duplicates. If we find one, stop processing here.
|
// First, check for duplicates. If we find one, stop processing here.
|
||||||
const inMemoryMessage = window.MessageController.findBySender(
|
const inMemoryMessage = window.MessageController.findBySender(
|
||||||
this.getSenderIdentifier()
|
this.getSenderIdentifier()
|
||||||
|
@ -2387,8 +2372,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
const messageId = message.get('id') || UUID.generate().toString();
|
const messageId = message.get('id') || UUID.generate().toString();
|
||||||
|
|
||||||
// Send delivery receipts, but only for incoming sealed sender messages
|
// Send delivery receipts, but only for non-story sealed sender messages
|
||||||
// and not for messages from unaccepted conversations
|
// and not for messages from unaccepted conversations
|
||||||
if (
|
if (
|
||||||
type === 'incoming' &&
|
type === 'incoming' &&
|
||||||
this.get('unidentifiedDeliveryReceived') &&
|
this.get('unidentifiedDeliveryReceived') &&
|
||||||
|
|
|
@ -265,6 +265,7 @@ export type UnprocessedType = {
|
||||||
serverTimestamp?: number;
|
serverTimestamp?: number;
|
||||||
decrypted?: string;
|
decrypted?: string;
|
||||||
urgent?: boolean;
|
urgent?: boolean;
|
||||||
|
story?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UnprocessedUpdateType = {
|
export type UnprocessedUpdateType = {
|
||||||
|
|
|
@ -3178,6 +3178,7 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
||||||
serverTimestamp,
|
serverTimestamp,
|
||||||
decrypted,
|
decrypted,
|
||||||
urgent,
|
urgent,
|
||||||
|
story,
|
||||||
} = data;
|
} = data;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('saveUnprocessedSync: id was falsey');
|
throw new Error('saveUnprocessedSync: id was falsey');
|
||||||
|
@ -3204,7 +3205,8 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
||||||
serverGuid,
|
serverGuid,
|
||||||
serverTimestamp,
|
serverTimestamp,
|
||||||
decrypted,
|
decrypted,
|
||||||
urgent
|
urgent,
|
||||||
|
story
|
||||||
) values (
|
) values (
|
||||||
$id,
|
$id,
|
||||||
$timestamp,
|
$timestamp,
|
||||||
|
@ -3218,7 +3220,8 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
||||||
$serverGuid,
|
$serverGuid,
|
||||||
$serverTimestamp,
|
$serverTimestamp,
|
||||||
$decrypted,
|
$decrypted,
|
||||||
$urgent
|
$urgent,
|
||||||
|
$story
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
).run({
|
).run({
|
||||||
|
@ -3235,6 +3238,7 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
||||||
serverTimestamp: serverTimestamp || null,
|
serverTimestamp: serverTimestamp || null,
|
||||||
decrypted: decrypted || null,
|
decrypted: decrypted || null,
|
||||||
urgent: urgent || !isBoolean(urgent) ? 1 : 0,
|
urgent: urgent || !isBoolean(urgent) ? 1 : 0,
|
||||||
|
story: story ? 1 : 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
@ -3309,6 +3313,7 @@ async function getUnprocessedById(
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
|
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
|
||||||
|
story: Boolean(row.story),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3370,6 +3375,7 @@ async function getAllUnprocessedAndIncrementAttempts(): Promise<
|
||||||
.map(row => ({
|
.map(row => ({
|
||||||
...row,
|
...row,
|
||||||
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
|
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
|
||||||
|
story: Boolean(row.story),
|
||||||
}));
|
}));
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
28
ts/sql/migrations/67-add-story-to-unprocessed.ts
Normal file
28
ts/sql/migrations/67-add-story-to-unprocessed.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Database } from 'better-sqlite3';
|
||||||
|
|
||||||
|
import type { LoggerType } from '../../types/Logging';
|
||||||
|
|
||||||
|
export default function updateToSchemaVersion67(
|
||||||
|
currentVersion: number,
|
||||||
|
db: Database,
|
||||||
|
logger: LoggerType
|
||||||
|
): void {
|
||||||
|
if (currentVersion >= 67) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.exec(
|
||||||
|
`
|
||||||
|
ALTER TABLE unprocessed ADD COLUMN story INTEGER;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
db.pragma('user_version = 67');
|
||||||
|
})();
|
||||||
|
|
||||||
|
logger.info('updateToSchemaVersion67: success!');
|
||||||
|
}
|
|
@ -42,6 +42,7 @@ import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
|
||||||
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
|
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
|
||||||
import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
|
import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
|
||||||
import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos';
|
import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos';
|
||||||
|
import updateToSchemaVersion67 from './67-add-story-to-unprocessed';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -1947,6 +1948,7 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion64,
|
updateToSchemaVersion64,
|
||||||
updateToSchemaVersion65,
|
updateToSchemaVersion65,
|
||||||
updateToSchemaVersion66,
|
updateToSchemaVersion66,
|
||||||
|
updateToSchemaVersion67,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const debug = createDebug('mock:test:rate-limit');
|
||||||
|
|
||||||
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||||
|
|
||||||
describe('rate-limit/story', function needsName() {
|
describe('story/no-sender-key', function needsName() {
|
||||||
this.timeout(durations.MINUTE);
|
this.timeout(durations.MINUTE);
|
||||||
|
|
||||||
let bootstrap: Bootstrap;
|
let bootstrap: Bootstrap;
|
||||||
|
@ -65,7 +65,7 @@ describe('rate-limit/story', function needsName() {
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should request challenge and accept solution', async () => {
|
it('should successfully send story', async () => {
|
||||||
const {
|
const {
|
||||||
server,
|
server,
|
||||||
contactsWithoutProfileKey: contacts,
|
contactsWithoutProfileKey: contacts,
|
||||||
|
@ -115,29 +115,6 @@ describe('rate-limit/story', function needsName() {
|
||||||
await window.locator('button.SendStoryModal__send').click();
|
await window.locator('button.SendStoryModal__send').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Waiting for challenge');
|
|
||||||
const request = await app.waitForChallenge();
|
|
||||||
|
|
||||||
debug('Checking for presence of captcha modal');
|
|
||||||
await window
|
|
||||||
.locator('.module-Modal__title >> "Verify to continue messaging"')
|
|
||||||
.waitFor();
|
|
||||||
|
|
||||||
debug('Removing rate-limiting');
|
|
||||||
for (const contact of contacts) {
|
|
||||||
const failedMessages = server.stopRateLimiting({
|
|
||||||
source: desktop.uuid,
|
|
||||||
target: contact.device.uuid,
|
|
||||||
});
|
|
||||||
assert.isAtMost(failedMessages ?? 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Solving challenge');
|
|
||||||
app.solveChallenge({
|
|
||||||
seq: request.seq,
|
|
||||||
data: { captcha: 'anything' },
|
|
||||||
});
|
|
||||||
|
|
||||||
debug('Verifying that all contacts received story');
|
debug('Verifying that all contacts received story');
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
contacts.map(async contact => {
|
contacts.map(async contact => {
|
||||||
|
|
|
@ -52,7 +52,6 @@ import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { UUID, UUIDKind } from '../types/UUID';
|
import { UUID, UUIDKind } from '../types/UUID';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import { isEnabled } from '../RemoteConfig';
|
|
||||||
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
||||||
|
@ -115,6 +114,8 @@ import { areArraysMatchingSets } from '../util/areArraysMatchingSets';
|
||||||
import { generateBlurHash } from '../util/generateBlurHash';
|
import { generateBlurHash } from '../util/generateBlurHash';
|
||||||
import { TEXT_ATTACHMENT } from '../types/MIME';
|
import { TEXT_ATTACHMENT } from '../types/MIME';
|
||||||
import type { SendTypesType } from '../util/handleMessageSend';
|
import type { SendTypesType } from '../util/handleMessageSend';
|
||||||
|
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||||
|
import { getStoriesBlocked } from '../types/Stories';
|
||||||
|
|
||||||
const GROUPV1_ID_LENGTH = 16;
|
const GROUPV1_ID_LENGTH = 16;
|
||||||
const GROUPV2_ID_LENGTH = 32;
|
const GROUPV2_ID_LENGTH = 32;
|
||||||
|
@ -394,6 +395,7 @@ export default class MessageReceiver
|
||||||
serverGuid: decoded.serverGuid,
|
serverGuid: decoded.serverGuid,
|
||||||
serverTimestamp,
|
serverTimestamp,
|
||||||
urgent: isBoolean(decoded.urgent) ? decoded.urgent : true,
|
urgent: isBoolean(decoded.urgent) ? decoded.urgent : true,
|
||||||
|
story: decoded.story,
|
||||||
};
|
};
|
||||||
|
|
||||||
// After this point, decoding errors are not the server's
|
// After this point, decoding errors are not the server's
|
||||||
|
@ -777,6 +779,7 @@ export default class MessageReceiver
|
||||||
serverTimestamp:
|
serverTimestamp:
|
||||||
item.serverTimestamp || decoded.serverTimestamp?.toNumber(),
|
item.serverTimestamp || decoded.serverTimestamp?.toNumber(),
|
||||||
urgent: isBoolean(item.urgent) ? item.urgent : true,
|
urgent: isBoolean(item.urgent) ? item.urgent : true,
|
||||||
|
story: Boolean(item.story),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { decrypted } = item;
|
const { decrypted } = item;
|
||||||
|
@ -1043,6 +1046,7 @@ export default class MessageReceiver
|
||||||
receivedAtCounter: envelope.receivedAtCounter,
|
receivedAtCounter: envelope.receivedAtCounter,
|
||||||
timestamp: envelope.timestamp,
|
timestamp: envelope.timestamp,
|
||||||
urgent: envelope.urgent,
|
urgent: envelope.urgent,
|
||||||
|
story: envelope.story,
|
||||||
};
|
};
|
||||||
this.decryptAndCacheBatcher.add({
|
this.decryptAndCacheBatcher.add({
|
||||||
request,
|
request,
|
||||||
|
@ -1271,10 +1275,10 @@ export default class MessageReceiver
|
||||||
envelope: UnsealedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
uuidKind: UUIDKind
|
uuidKind: UUIDKind
|
||||||
): Promise<DecryptResult> {
|
): Promise<DecryptResult> {
|
||||||
const logId = getEnvelopeId(envelope);
|
const logId = `MessageReceiver.decryptEnvelope(${getEnvelopeId(envelope)})`;
|
||||||
|
|
||||||
if (this.stoppingProcessing) {
|
if (this.stoppingProcessing) {
|
||||||
log.warn(`MessageReceiver.decryptEnvelope(${logId}): dropping unsealed`);
|
log.warn(`${logId}: dropping unsealed`);
|
||||||
throw new Error('Unsealed envelope dropped due to stopping processing');
|
throw new Error('Unsealed envelope dropped due to stopping processing');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1298,7 +1302,7 @@ export default class MessageReceiver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`MessageReceiver.decryptEnvelope(${logId})`);
|
log.info(logId);
|
||||||
const plaintext = await this.decrypt(
|
const plaintext = await this.decrypt(
|
||||||
stores,
|
stores,
|
||||||
envelope,
|
envelope,
|
||||||
|
@ -1307,7 +1311,7 @@ export default class MessageReceiver
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!plaintext) {
|
if (!plaintext) {
|
||||||
log.warn('MessageReceiver.decryptEnvelope: plaintext was falsey');
|
log.warn(`${logId}: plaintext was falsey`);
|
||||||
return { plaintext, envelope };
|
return { plaintext, envelope };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1331,6 +1335,53 @@ export default class MessageReceiver
|
||||||
envelope,
|
envelope,
|
||||||
content.senderKeyDistributionMessage
|
content.senderKeyDistributionMessage
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Note: `story = true` can be set for sender key distribution messages
|
||||||
|
|
||||||
|
const isStoryReply = Boolean(content.dataMessage?.storyContext);
|
||||||
|
const isGroupStoryReply = Boolean(
|
||||||
|
isStoryReply && content.dataMessage?.groupV2
|
||||||
|
);
|
||||||
|
const isStory = Boolean(content.storyMessage);
|
||||||
|
const isGroupStorySend = isGroupStoryReply || isStory;
|
||||||
|
const isDeleteForEveryone = Boolean(content.dataMessage?.delete);
|
||||||
|
|
||||||
|
if (envelope.story && !isGroupStorySend && !isDeleteForEveryone) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: Dropping story message - story=true on envelope, but message was not a group story send or delete`
|
||||||
|
);
|
||||||
|
this.removeFromCache(envelope);
|
||||||
|
return { plaintext: undefined, envelope };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelope.story && isGroupStorySend) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: Malformed story - story=false on envelope, but was a group story send`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const areStoriesBlocked = getStoriesBlocked();
|
||||||
|
// Note that there are other story-related message types which aren't captured
|
||||||
|
// here. Look for other calls to getStoriesBlocked down-file.
|
||||||
|
if (areStoriesBlocked && (isStoryReply || isStory)) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: Dropping story message - stories are disabled or unavailable`
|
||||||
|
);
|
||||||
|
this.removeFromCache(envelope);
|
||||||
|
return { plaintext: undefined, envelope };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = window.ConversationController.get(
|
||||||
|
envelope.sourceUuid || envelope.source
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
(!sender || !isConversationAccepted(sender.attributes)) &&
|
||||||
|
(isStoryReply || isStory)
|
||||||
|
) {
|
||||||
|
log.warn(`${logId}: Dropping story message - !accepted for sender`);
|
||||||
|
this.removeFromCache(envelope);
|
||||||
|
return { plaintext: undefined, envelope };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.pniSignatureMessage) {
|
if (content.pniSignatureMessage) {
|
||||||
|
@ -1359,8 +1410,7 @@ export default class MessageReceiver
|
||||||
inProgressMessageType = '';
|
inProgressMessageType = '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
'MessageReceiver.decryptEnvelope: ' +
|
`${logId}: Failed to process ${inProgressMessageType} ` +
|
||||||
`Failed to process ${inProgressMessageType} ` +
|
|
||||||
`message: ${Errors.toLogFormat(error)}`
|
`message: ${Errors.toLogFormat(error)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1371,9 +1421,7 @@ export default class MessageReceiver
|
||||||
((envelope.source && this.isBlocked(envelope.source)) ||
|
((envelope.source && this.isBlocked(envelope.source)) ||
|
||||||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)))
|
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)))
|
||||||
) {
|
) {
|
||||||
log.info(
|
log.info(`${logId}: Dropping non-GV2 message from blocked sender`);
|
||||||
'MessageReceiver.decryptEnvelope: Dropping non-GV2 message from blocked sender'
|
|
||||||
);
|
|
||||||
return { plaintext: undefined, envelope };
|
return { plaintext: undefined, envelope };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1900,16 +1948,19 @@ export default class MessageReceiver
|
||||||
sentMessage?: ProcessedSent
|
sentMessage?: ProcessedSent
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const logId = getEnvelopeId(envelope);
|
const logId = getEnvelopeId(envelope);
|
||||||
log.info('MessageReceiver.handleStoryMessage', logId);
|
|
||||||
|
|
||||||
const attachments: Array<ProcessedAttachment> = [];
|
logUnexpectedUrgentValue(envelope, 'story');
|
||||||
|
|
||||||
if (window.Events.getHasStoriesDisabled()) {
|
if (getStoriesBlocked()) {
|
||||||
log.info('MessageReceiver.handleStoryMessage: dropping', logId);
|
log.info('MessageReceiver.handleStoryMessage: dropping', logId);
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info('MessageReceiver.handleStoryMessage', logId);
|
||||||
|
|
||||||
|
const attachments: Array<ProcessedAttachment> = [];
|
||||||
|
|
||||||
if (msg.fileAttachment) {
|
if (msg.fileAttachment) {
|
||||||
const attachment = processAttachment(msg.fileAttachment);
|
const attachment = processAttachment(msg.fileAttachment);
|
||||||
attachments.push(attachment);
|
attachments.push(attachment);
|
||||||
|
@ -2076,16 +2127,12 @@ export default class MessageReceiver
|
||||||
const logId = getEnvelopeId(envelope);
|
const logId = getEnvelopeId(envelope);
|
||||||
log.info('MessageReceiver.handleDataMessage', logId);
|
log.info('MessageReceiver.handleDataMessage', logId);
|
||||||
|
|
||||||
const isStoriesEnabled =
|
if (getStoriesBlocked() && msg.storyContext) {
|
||||||
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
|
|
||||||
if (!isStoriesEnabled && msg.storyContext) {
|
|
||||||
logUnexpectedUrgentValue(envelope, 'story');
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`MessageReceiver.handleDataMessage/${logId}: Dropping incoming dataMessage with storyContext field`
|
`MessageReceiver.handleDataMessage/${logId}: Dropping incoming dataMessage with storyContext field`
|
||||||
);
|
);
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let p: Promise<void> = Promise.resolve();
|
let p: Promise<void> = Promise.resolve();
|
||||||
|
@ -2129,9 +2176,7 @@ export default class MessageReceiver
|
||||||
|
|
||||||
let type: SendTypesType = 'message';
|
let type: SendTypesType = 'message';
|
||||||
|
|
||||||
if (msg.storyContext) {
|
if (msg.storyContext || msg.body) {
|
||||||
type = 'story';
|
|
||||||
} else if (msg.body) {
|
|
||||||
type = 'message';
|
type = 'message';
|
||||||
} else if (msg.reaction) {
|
} else if (msg.reaction) {
|
||||||
type = 'reaction';
|
type = 'reaction';
|
||||||
|
@ -2294,19 +2339,8 @@ export default class MessageReceiver
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStoriesEnabled =
|
|
||||||
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
|
|
||||||
if (content.storyMessage) {
|
if (content.storyMessage) {
|
||||||
if (isStoriesEnabled) {
|
await this.handleStoryMessage(envelope, content.storyMessage);
|
||||||
await this.handleStoryMessage(envelope, content.storyMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logId = getEnvelopeId(envelope);
|
|
||||||
log.info(
|
|
||||||
`innerHandleContentMessage/${logId}: Dropping incoming message with storyMessage field`
|
|
||||||
);
|
|
||||||
this.removeFromCache(envelope);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2689,16 +2723,17 @@ export default class MessageReceiver
|
||||||
const sentMessage = syncMessage.sent;
|
const sentMessage = syncMessage.sent;
|
||||||
|
|
||||||
if (sentMessage.storyMessageRecipients && sentMessage.isRecipientUpdate) {
|
if (sentMessage.storyMessageRecipients && sentMessage.isRecipientUpdate) {
|
||||||
if (window.Events.getHasStoriesDisabled()) {
|
if (getStoriesBlocked()) {
|
||||||
log.info(
|
log.info(
|
||||||
'MessageReceiver.handleSyncMessage: dropping story recipients update'
|
'MessageReceiver.handleSyncMessage: dropping story recipients update',
|
||||||
|
getEnvelopeId(envelope)
|
||||||
);
|
);
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'MessageReceiver.handleSyncMessage: handling storyMessageRecipients isRecipientUpdate sync message',
|
'MessageReceiver.handleSyncMessage: handling story recipients update',
|
||||||
getEnvelopeId(envelope)
|
getEnvelopeId(envelope)
|
||||||
);
|
);
|
||||||
const ev = new StoryRecipientUpdateEvent(
|
const ev = new StoryRecipientUpdateEvent(
|
||||||
|
|
|
@ -1255,6 +1255,7 @@ export default class MessageSender {
|
||||||
groupId,
|
groupId,
|
||||||
options,
|
options,
|
||||||
urgent,
|
urgent,
|
||||||
|
story,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
recipients: Array<string>;
|
recipients: Array<string>;
|
||||||
|
@ -1263,6 +1264,7 @@ export default class MessageSender {
|
||||||
groupId: string | undefined;
|
groupId: string | undefined;
|
||||||
options?: SendOptionsType;
|
options?: SendOptionsType;
|
||||||
urgent: boolean;
|
urgent: boolean;
|
||||||
|
story?: boolean;
|
||||||
}>): Promise<CallbackResultType> {
|
}>): Promise<CallbackResultType> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const callback = (result: CallbackResultType) => {
|
const callback = (result: CallbackResultType) => {
|
||||||
|
@ -1282,6 +1284,7 @@ export default class MessageSender {
|
||||||
recipients,
|
recipients,
|
||||||
timestamp,
|
timestamp,
|
||||||
urgent,
|
urgent,
|
||||||
|
story,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
1
ts/textsecure/Types.d.ts
vendored
1
ts/textsecure/Types.d.ts
vendored
|
@ -94,6 +94,7 @@ export type ProcessedEnvelope = Readonly<{
|
||||||
serverTimestamp: number;
|
serverTimestamp: number;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
urgent?: boolean;
|
urgent?: boolean;
|
||||||
|
story?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ProcessedAttachment = {
|
export type ProcessedAttachment = {
|
||||||
|
|
|
@ -942,6 +942,7 @@ export type WebAPIType = {
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
options: {
|
options: {
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
|
story?: boolean;
|
||||||
urgent?: boolean;
|
urgent?: boolean;
|
||||||
}
|
}
|
||||||
) => Promise<MultiRecipient200ResponseType>;
|
) => Promise<MultiRecipient200ResponseType>;
|
||||||
|
@ -2122,14 +2123,13 @@ export function initialize({
|
||||||
messages,
|
messages,
|
||||||
timestamp,
|
timestamp,
|
||||||
online: Boolean(online),
|
online: Boolean(online),
|
||||||
story,
|
|
||||||
urgent,
|
urgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
await _ajax({
|
await _ajax({
|
||||||
call: 'messages',
|
call: 'messages',
|
||||||
httpType: 'PUT',
|
httpType: 'PUT',
|
||||||
urlParameters: `/${destination}`,
|
urlParameters: `/${destination}?story=${booleanToString(story)}`,
|
||||||
jsonData,
|
jsonData,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
unauthenticated: true,
|
unauthenticated: true,
|
||||||
|
@ -2151,14 +2151,13 @@ export function initialize({
|
||||||
messages,
|
messages,
|
||||||
timestamp,
|
timestamp,
|
||||||
online: Boolean(online),
|
online: Boolean(online),
|
||||||
story,
|
|
||||||
urgent,
|
urgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
await _ajax({
|
await _ajax({
|
||||||
call: 'messages',
|
call: 'messages',
|
||||||
httpType: 'PUT',
|
httpType: 'PUT',
|
||||||
urlParameters: `/${destination}`,
|
urlParameters: `/${destination}?story=${booleanToString(story)}`,
|
||||||
jsonData,
|
jsonData,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
});
|
});
|
||||||
|
@ -2175,20 +2174,23 @@ export function initialize({
|
||||||
{
|
{
|
||||||
online,
|
online,
|
||||||
urgent = true,
|
urgent = true,
|
||||||
|
story = false,
|
||||||
}: {
|
}: {
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
|
story?: boolean;
|
||||||
urgent?: boolean;
|
urgent?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<MultiRecipient200ResponseType> {
|
): Promise<MultiRecipient200ResponseType> {
|
||||||
const onlineParam = `&online=${booleanToString(online)}`;
|
const onlineParam = `&online=${booleanToString(online)}`;
|
||||||
const urgentParam = `&urgent=${booleanToString(urgent)}`;
|
const urgentParam = `&urgent=${booleanToString(urgent)}`;
|
||||||
|
const storyParam = `&story=${booleanToString(story)}`;
|
||||||
|
|
||||||
const response = await _ajax({
|
const response = await _ajax({
|
||||||
call: 'multiRecipient',
|
call: 'multiRecipient',
|
||||||
httpType: 'PUT',
|
httpType: 'PUT',
|
||||||
contentType: 'application/vnd.signal-messenger.mrm',
|
contentType: 'application/vnd.signal-messenger.mrm',
|
||||||
data,
|
data,
|
||||||
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}`,
|
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
unauthenticated: true,
|
unauthenticated: true,
|
||||||
accessKey: Bytes.toBase64(accessKeys),
|
accessKey: Bytes.toBase64(accessKeys),
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import type { SendStatus } from '../messages/MessageSendState';
|
import type { SendStatus } from '../messages/MessageSendState';
|
||||||
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
||||||
import type { UUIDStringType } from './UUID';
|
import type { UUIDStringType } from './UUID';
|
||||||
|
import { isEnabled } from '../RemoteConfig';
|
||||||
|
|
||||||
export type ReplyType = {
|
export type ReplyType = {
|
||||||
author: Pick<
|
author: Pick<
|
||||||
|
@ -142,3 +143,9 @@ export enum HasStories {
|
||||||
Read = 'Read',
|
Read = 'Read',
|
||||||
Unread = 'Unread',
|
Unread = 'Unread',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStoriesAvailable = () =>
|
||||||
|
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
|
||||||
|
const getStoriesDisabled = () => window.Events.getHasStoriesDisabled();
|
||||||
|
export const getStoriesBlocked = (): boolean =>
|
||||||
|
!getStoriesAvailable() || getStoriesDisabled();
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
||||||
SendOptionsType,
|
SendOptionsType,
|
||||||
} from '../textsecure/SendMessage';
|
} from '../textsecure/SendMessage';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import { getRandomBytes } from '../Crypto';
|
import { getRandomBytes, getZeroes } from '../Crypto';
|
||||||
import { getConversationMembers } from './getConversationMembers';
|
import { getConversationMembers } from './getConversationMembers';
|
||||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
||||||
import { senderCertificateService } from '../services/senderCertificate';
|
import { senderCertificateService } from '../services/senderCertificate';
|
||||||
|
@ -24,14 +24,17 @@ const SEALED_SENDER = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getSendOptionsForRecipients(
|
export async function getSendOptionsForRecipients(
|
||||||
recipients: ReadonlyArray<string>
|
recipients: ReadonlyArray<string>,
|
||||||
|
options?: { story?: boolean }
|
||||||
): Promise<SendOptionsType> {
|
): Promise<SendOptionsType> {
|
||||||
const conversations = recipients
|
const conversations = recipients
|
||||||
.map(identifier => window.ConversationController.get(identifier))
|
.map(identifier => window.ConversationController.get(identifier))
|
||||||
.filter(isNotNil);
|
.filter(isNotNil);
|
||||||
|
|
||||||
const metadataList = await Promise.all(
|
const metadataList = await Promise.all(
|
||||||
conversations.map(conversation => getSendOptions(conversation.attributes))
|
conversations.map(conversation =>
|
||||||
|
getSendOptions(conversation.attributes, options)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return metadataList.reduce(
|
return metadataList.reduce(
|
||||||
|
@ -58,9 +61,9 @@ export async function getSendOptionsForRecipients(
|
||||||
|
|
||||||
export async function getSendOptions(
|
export async function getSendOptions(
|
||||||
conversationAttrs: ConversationAttributesType,
|
conversationAttrs: ConversationAttributesType,
|
||||||
options: { syncMessage?: boolean } = {}
|
options: { syncMessage?: boolean; story?: boolean } = {}
|
||||||
): Promise<SendOptionsType> {
|
): Promise<SendOptionsType> {
|
||||||
const { syncMessage } = options;
|
const { syncMessage, story } = options;
|
||||||
|
|
||||||
if (!isDirectConversation(conversationAttrs)) {
|
if (!isDirectConversation(conversationAttrs)) {
|
||||||
const contactCollection = getConversationMembers(conversationAttrs);
|
const contactCollection = getConversationMembers(conversationAttrs);
|
||||||
|
@ -97,9 +100,13 @@ export async function getSendOptions(
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we've never fetched user's profile, we default to what we have
|
// If we've never fetched user's profile, we default to what we have
|
||||||
if (sealedSender === SEALED_SENDER.UNKNOWN) {
|
if (sealedSender === SEALED_SENDER.UNKNOWN || story) {
|
||||||
const identifierData = {
|
const identifierData = {
|
||||||
accessKey: accessKey || Bytes.toBase64(getRandomBytes(16)),
|
accessKey:
|
||||||
|
accessKey ||
|
||||||
|
(story
|
||||||
|
? Bytes.toBase64(getZeroes(16))
|
||||||
|
: Bytes.toBase64(getRandomBytes(16))),
|
||||||
senderCertificate,
|
senderCertificate,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -38,6 +38,7 @@ import type {
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import MessageSender from '../textsecure/SendMessage';
|
import MessageSender from '../textsecure/SendMessage';
|
||||||
|
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
||||||
|
|
||||||
const RETRY_LIMIT = 5;
|
const RETRY_LIMIT = 5;
|
||||||
|
|
||||||
|
@ -138,7 +139,8 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
||||||
|
|
||||||
const { contentHint, messageIds, proto, timestamp, urgent } = sentProto;
|
const { contentHint, messageIds, proto, timestamp, urgent } = sentProto;
|
||||||
|
|
||||||
const { contentProto, groupId } = await maybeAddSenderKeyDistributionMessage({
|
// Only applies to sender key sends in groups. See below for story distribution lists.
|
||||||
|
const addSenderKeyResult = await maybeAddSenderKeyDistributionMessage({
|
||||||
contentProto: Proto.Content.decode(proto),
|
contentProto: Proto.Content.decode(proto),
|
||||||
logId,
|
logId,
|
||||||
messageIds,
|
messageIds,
|
||||||
|
@ -146,44 +148,35 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
||||||
requesterUuid,
|
requesterUuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
let contentProto: Proto.IContent | undefined =
|
||||||
|
addSenderKeyResult.contentProto;
|
||||||
|
const { groupId } = addSenderKeyResult;
|
||||||
|
|
||||||
// Assert that the requesting UUID is still part of a distribution list that
|
// Assert that the requesting UUID is still part of a story distribution list that
|
||||||
// the message was sent to.
|
// the message was sent to, and add its sender key distribution message (SKDM).
|
||||||
if (contentProto.storyMessage) {
|
if (contentProto.storyMessage && !groupId) {
|
||||||
const { storyDistributionLists } = window.reduxStore.getState();
|
contentProto = await checkDistributionListAndAddSKDM({
|
||||||
const membersByListId = new Map<string, Set<string>>();
|
confirm,
|
||||||
storyDistributionLists.distributionLists.forEach(list => {
|
contentProto,
|
||||||
membersByListId.set(list.id, new Set(list.memberUuids));
|
logId,
|
||||||
|
messaging,
|
||||||
|
requesterUuid,
|
||||||
|
timestamp,
|
||||||
});
|
});
|
||||||
|
if (!contentProto) {
|
||||||
const messages = await dataInterface.getMessagesBySentAt(timestamp);
|
|
||||||
const isInDistributionList = messages.some(message => {
|
|
||||||
if (!message.storyDistributionListId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = membersByListId.get(message.storyDistributionListId);
|
|
||||||
if (!members) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return members.has(requesterUuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isInDistributionList) {
|
|
||||||
log.warn(
|
|
||||||
`onRetryRequest/${logId}: requesterUuid is not in distribution list`
|
|
||||||
);
|
|
||||||
confirm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const story = Boolean(contentProto.storyMessage);
|
||||||
|
|
||||||
const recipientConversation = window.ConversationController.getOrCreate(
|
const recipientConversation = window.ConversationController.getOrCreate(
|
||||||
requesterUuid,
|
requesterUuid,
|
||||||
'private'
|
'private'
|
||||||
);
|
);
|
||||||
const sendOptions = await getSendOptions(recipientConversation.attributes);
|
const sendOptions = await getSendOptions(recipientConversation.attributes, {
|
||||||
|
story,
|
||||||
|
});
|
||||||
const promise = messaging.sendMessageProtoAndWait({
|
const promise = messaging.sendMessageProtoAndWait({
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
|
@ -192,6 +185,7 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
||||||
recipients: [requesterUuid],
|
recipients: [requesterUuid],
|
||||||
timestamp,
|
timestamp,
|
||||||
urgent,
|
urgent,
|
||||||
|
story,
|
||||||
});
|
});
|
||||||
|
|
||||||
await handleMessageSend(promise, {
|
await handleMessageSend(promise, {
|
||||||
|
@ -427,6 +421,88 @@ async function getRetryConversation({
|
||||||
return window.ConversationController.get(conversationId);
|
return window.ConversationController.get(conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkDistributionListAndAddSKDM({
|
||||||
|
contentProto,
|
||||||
|
timestamp,
|
||||||
|
confirm,
|
||||||
|
logId,
|
||||||
|
requesterUuid,
|
||||||
|
messaging,
|
||||||
|
}: {
|
||||||
|
contentProto: Proto.IContent;
|
||||||
|
timestamp: number;
|
||||||
|
confirm: () => void;
|
||||||
|
requesterUuid: string;
|
||||||
|
logId: string;
|
||||||
|
messaging: MessageSender;
|
||||||
|
}): Promise<Proto.IContent | undefined> {
|
||||||
|
let distributionList: StoryDistributionListDataType | undefined;
|
||||||
|
const { storyDistributionLists } = window.reduxStore.getState();
|
||||||
|
const membersByListId = new Map<string, Set<string>>();
|
||||||
|
const listsById = new Map<string, StoryDistributionListDataType>();
|
||||||
|
storyDistributionLists.distributionLists.forEach(list => {
|
||||||
|
membersByListId.set(list.id, new Set(list.memberUuids));
|
||||||
|
listsById.set(list.id, list);
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages = await dataInterface.getMessagesBySentAt(timestamp);
|
||||||
|
const isInAnyDistributionList = messages.some(message => {
|
||||||
|
const listId = message.storyDistributionListId;
|
||||||
|
if (!listId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = membersByListId.get(listId);
|
||||||
|
if (!members) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInList = members.has(requesterUuid);
|
||||||
|
|
||||||
|
if (isInList) {
|
||||||
|
distributionList = listsById.get(listId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInList;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isInAnyDistributionList) {
|
||||||
|
log.warn(
|
||||||
|
`checkDistributionListAndAddSKDM/${logId}: requesterUuid is not in distribution list. Dropping.`
|
||||||
|
);
|
||||||
|
confirm();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
strictAssert(
|
||||||
|
distributionList,
|
||||||
|
`checkDistributionListAndAddSKDM/${logId}: Should have a distribution list by this point`
|
||||||
|
);
|
||||||
|
const distributionDetails =
|
||||||
|
await window.Signal.Data.getStoryDistributionWithMembers(
|
||||||
|
distributionList.id
|
||||||
|
);
|
||||||
|
const distributionId = distributionDetails?.senderKeyInfo?.distributionId;
|
||||||
|
if (!distributionId) {
|
||||||
|
log.warn(
|
||||||
|
`onRetryRequest/${logId}: No sender key info for distribution list ${distributionList.id}`
|
||||||
|
);
|
||||||
|
return contentProto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protoWithDistributionMessage =
|
||||||
|
await messaging.getSenderKeyDistributionMessage(distributionId, {
|
||||||
|
throwIfNotInDatabase: true,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...contentProto,
|
||||||
|
senderKeyDistributionMessage:
|
||||||
|
protoWithDistributionMessage.senderKeyDistributionMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function maybeAddSenderKeyDistributionMessage({
|
async function maybeAddSenderKeyDistributionMessage({
|
||||||
contentProto,
|
contentProto,
|
||||||
logId,
|
logId,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { UUIDStringType } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
import { DAY, SECOND } from './durations';
|
import { DAY, SECOND } from './durations';
|
||||||
import { MY_STORIES_ID } from '../types/Stories';
|
import { getStoriesBlocked, MY_STORIES_ID } from '../types/Stories';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import { SeenStatus } from '../MessageSeenStatus';
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
|
@ -28,10 +28,17 @@ export async function sendStoryMessage(
|
||||||
conversationIds: Array<string>,
|
conversationIds: Array<string>,
|
||||||
attachment: AttachmentType
|
attachment: AttachmentType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (getStoriesBlocked()) {
|
||||||
|
log.warn('stories.sendStoryMessage: stories disabled, returning early');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { messaging } = window.textsecure;
|
const { messaging } = window.textsecure;
|
||||||
|
|
||||||
if (!messaging) {
|
if (!messaging) {
|
||||||
log.warn('stories.sendStoryMessage: messaging not available');
|
log.warn(
|
||||||
|
'stories.sendStoryMessage: messaging not available, returning early'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ export async function sendToGroup({
|
||||||
sendOptions,
|
sendOptions,
|
||||||
sendTarget,
|
sendTarget,
|
||||||
sendType,
|
sendType,
|
||||||
|
story,
|
||||||
urgent,
|
urgent,
|
||||||
}: {
|
}: {
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
|
@ -109,6 +110,7 @@ export async function sendToGroup({
|
||||||
sendOptions?: SendOptionsType;
|
sendOptions?: SendOptionsType;
|
||||||
sendTarget: SenderKeyTargetType;
|
sendTarget: SenderKeyTargetType;
|
||||||
sendType: SendTypesType;
|
sendType: SendTypesType;
|
||||||
|
story?: boolean;
|
||||||
urgent: boolean;
|
urgent: boolean;
|
||||||
}): Promise<CallbackResultType> {
|
}): Promise<CallbackResultType> {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
@ -141,6 +143,7 @@ export async function sendToGroup({
|
||||||
sendOptions,
|
sendOptions,
|
||||||
sendTarget,
|
sendTarget,
|
||||||
sendType,
|
sendType,
|
||||||
|
story,
|
||||||
timestamp,
|
timestamp,
|
||||||
urgent,
|
urgent,
|
||||||
});
|
});
|
||||||
|
@ -377,7 +380,7 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
// 4. Partition devices into sender key and non-sender key groups
|
// 4. Partition devices into sender key and non-sender key groups
|
||||||
const [devicesForSenderKey, devicesForNormalSend] = partition(
|
const [devicesForSenderKey, devicesForNormalSend] = partition(
|
||||||
currentDevices,
|
currentDevices,
|
||||||
device => isValidSenderKeyRecipient(memberSet, device.identifier)
|
device => isValidSenderKeyRecipient(memberSet, device.identifier, { story })
|
||||||
);
|
);
|
||||||
|
|
||||||
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
||||||
|
@ -513,13 +516,13 @@ export async function sendToGroupViaSenderKey(options: {
|
||||||
contentMessage: Proto.Content.encode(contentMessage).finish(),
|
contentMessage: Proto.Content.encode(contentMessage).finish(),
|
||||||
groupId,
|
groupId,
|
||||||
});
|
});
|
||||||
const accessKeys = getXorOfAccessKeys(devicesForSenderKey);
|
const accessKeys = getXorOfAccessKeys(devicesForSenderKey, { story });
|
||||||
|
|
||||||
const result = await window.textsecure.messaging.server.sendWithSenderKey(
|
const result = await window.textsecure.messaging.server.sendWithSenderKey(
|
||||||
messageBuffer,
|
messageBuffer,
|
||||||
accessKeys,
|
accessKeys,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ online, urgent }
|
{ online, story, urgent }
|
||||||
);
|
);
|
||||||
|
|
||||||
const parsed = multiRecipient200ResponseSchema.safeParse(result);
|
const parsed = multiRecipient200ResponseSchema.safeParse(result);
|
||||||
|
@ -977,7 +980,10 @@ async function handle410Response(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getXorOfAccessKeys(devices: Array<DeviceType>): Buffer {
|
function getXorOfAccessKeys(
|
||||||
|
devices: Array<DeviceType>,
|
||||||
|
{ story }: { story?: boolean } = {}
|
||||||
|
): Buffer {
|
||||||
const uuids = getUuidsFromDevices(devices);
|
const uuids = getUuidsFromDevices(devices);
|
||||||
|
|
||||||
const result = Buffer.alloc(ACCESS_KEY_LENGTH);
|
const result = Buffer.alloc(ACCESS_KEY_LENGTH);
|
||||||
|
@ -994,7 +1000,7 @@ function getXorOfAccessKeys(devices: Array<DeviceType>): Buffer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessKey = getAccessKey(conversation.attributes);
|
const accessKey = getAccessKey(conversation.attributes, { story });
|
||||||
if (!accessKey) {
|
if (!accessKey) {
|
||||||
throw new Error(`getXorOfAccessKeys: No accessKey for UUID ${uuid}`);
|
throw new Error(`getXorOfAccessKeys: No accessKey for UUID ${uuid}`);
|
||||||
}
|
}
|
||||||
|
@ -1099,7 +1105,8 @@ async function encryptForSenderKey({
|
||||||
|
|
||||||
function isValidSenderKeyRecipient(
|
function isValidSenderKeyRecipient(
|
||||||
members: Set<ConversationModel>,
|
members: Set<ConversationModel>,
|
||||||
uuid: string
|
uuid: string,
|
||||||
|
{ story }: { story?: boolean } = {}
|
||||||
): boolean {
|
): boolean {
|
||||||
const memberConversation = window.ConversationController.get(uuid);
|
const memberConversation = window.ConversationController.get(uuid);
|
||||||
if (!memberConversation) {
|
if (!memberConversation) {
|
||||||
|
@ -1121,7 +1128,7 @@ function isValidSenderKeyRecipient(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getAccessKey(memberConversation.attributes)) {
|
if (!getAccessKey(memberConversation.attributes, { story })) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1247,10 +1254,15 @@ async function resetSenderKey(sendTarget: SenderKeyTargetType): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccessKey(
|
function getAccessKey(
|
||||||
attributes: ConversationAttributesType
|
attributes: ConversationAttributesType,
|
||||||
|
{ story }: { story?: boolean }
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const { sealedSender, accessKey } = attributes;
|
const { sealedSender, accessKey } = attributes;
|
||||||
|
|
||||||
|
if (story) {
|
||||||
|
return accessKey || ZERO_ACCESS_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
if (sealedSender === SEALED_SENDER.ENABLED) {
|
if (sealedSender === SEALED_SENDER.ENABLED) {
|
||||||
return accessKey || undefined;
|
return accessKey || undefined;
|
||||||
}
|
}
|
||||||
|
@ -1307,11 +1319,13 @@ async function fetchKeysForIdentifier(
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Note: we have no way to make an unrestricted unathenticated key fetch as part of a
|
||||||
|
// story send, so we hardcode story=false.
|
||||||
const { accessKeyFailed } = await getKeysForIdentifier(
|
const { accessKeyFailed } = await getKeysForIdentifier(
|
||||||
identifier,
|
identifier,
|
||||||
window.textsecure?.messaging?.server,
|
window.textsecure?.messaging?.server,
|
||||||
devices,
|
devices,
|
||||||
getAccessKey(emptyConversation.attributes)
|
getAccessKey(emptyConversation.attributes, { story: false })
|
||||||
);
|
);
|
||||||
if (accessKeyFailed) {
|
if (accessKeyFailed) {
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
@ -1975,10 +1975,10 @@
|
||||||
node-gyp-build "^4.2.3"
|
node-gyp-build "^4.2.3"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
"@signalapp/mock-server@2.10.0":
|
"@signalapp/mock-server@2.11.0":
|
||||||
version "2.10.0"
|
version "2.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.10.0.tgz#a27246e7b912caebc0bef628303e11689bf9b74c"
|
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.11.0.tgz#fe5f6229c4a5c28b3591e986a1622218452c5112"
|
||||||
integrity sha512-kHos3n8lNBhivUecEFG4g1rvYpJ6oPgzKMOsaI+vN8R1R4Pc63WXxrLsxqAI2QmAngD+nmOgbjwAvKyH4MN0+w==
|
integrity sha512-m23XZ8lrBn0u+zakxkKG5SezyUg6fnWwZewFF28sHNL7fQDVPHJkFCJZgE9XJwHBDM7TYz9ca/ucReW4GIPHoQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@signalapp/libsignal-client" "^0.20.0"
|
"@signalapp/libsignal-client" "^0.20.0"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue