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",
|
||||
"@electron/fuses": "1.5.0",
|
||||
"@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-actions": "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 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;
|
||||
// next: 16
|
||||
optional bool story = 16; // indicates that the content is a story.
|
||||
// next: 17
|
||||
}
|
||||
|
||||
message Content {
|
||||
|
|
|
@ -156,7 +156,8 @@ message AccountRecord {
|
|||
optional bool keepMutedChatsArchived = 25;
|
||||
optional bool hasSetMyStoriesPrivacy = 26;
|
||||
reserved /* hasViewedOnboardingStory */ 27;
|
||||
optional bool storiesDisabled = 28;
|
||||
reserved 28; // deprecatedStoriesDisabled
|
||||
optional bool storiesDisabled = 29;
|
||||
}
|
||||
|
||||
message StoryDistributionListRecord {
|
||||
|
|
|
@ -183,6 +183,7 @@ export async function sendNormalMessage(
|
|||
quote,
|
||||
recipients: allRecipientIdentifiers,
|
||||
sticker,
|
||||
// No storyContext; you can't reply to your own stories
|
||||
timestamp: messageTimestamp,
|
||||
});
|
||||
messageSendPromise = message.sendSyncMessageOnly(dataMessage, saveErrors);
|
||||
|
@ -234,6 +235,7 @@ export async function sendNormalMessage(
|
|||
sendOptions,
|
||||
sendTarget: conversation.toSenderKeyTarget(),
|
||||
sendType: 'message',
|
||||
story: Boolean(storyContext),
|
||||
urgent: true,
|
||||
})
|
||||
);
|
||||
|
@ -282,6 +284,7 @@ export async function sendNormalMessage(
|
|||
sticker,
|
||||
storyContext,
|
||||
timestamp: messageTimestamp,
|
||||
// Note: 1:1 story replies should not set story=true - they aren't group sends
|
||||
urgent: true,
|
||||
includePniSignatureMessage: true,
|
||||
});
|
||||
|
|
|
@ -264,7 +264,8 @@ export async function sendStory(
|
|||
const recipientsSet = new Set(pendingSendRecipientIds);
|
||||
|
||||
const sendOptions = await getSendOptionsForRecipients(
|
||||
pendingSendRecipientIds
|
||||
pendingSendRecipientIds,
|
||||
{ story: true }
|
||||
);
|
||||
|
||||
log.info(
|
||||
|
|
|
@ -179,11 +179,13 @@ export class Reactions extends Collection<ReactionModel> {
|
|||
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, {
|
||||
ourUuid: window.textsecure.storage.user
|
||||
.getCheckedUuid()
|
||||
.toString(),
|
||||
forceSave: true,
|
||||
}),
|
||||
generatedMessage.hydrateStoryContext(message),
|
||||
]);
|
||||
|
@ -197,10 +199,8 @@ export class Reactions extends Collection<ReactionModel> {
|
|||
timestamp: reaction.get('timestamp'),
|
||||
});
|
||||
|
||||
generatedMessage.set({ id: generatedMessageId });
|
||||
|
||||
const messageToAdd = window.MessageController.register(
|
||||
generatedMessageId,
|
||||
generatedMessage.id,
|
||||
generatedMessage
|
||||
);
|
||||
targetConversation.addSingleMessage(messageToAdd);
|
||||
|
|
|
@ -159,7 +159,6 @@ import { getMessageIdForLogging } from '../util/idForLogging';
|
|||
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
|
||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
||||
import { findStoryMessage } from '../util/findStoryMessage';
|
||||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
|
||||
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
|
@ -2097,20 +2096,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
await conversation.queueJob('handleDataMessage', async () => {
|
||||
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.
|
||||
const inMemoryMessage = window.MessageController.findBySender(
|
||||
this.getSenderIdentifier()
|
||||
|
@ -2387,7 +2372,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
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
|
||||
if (
|
||||
type === 'incoming' &&
|
||||
|
|
|
@ -265,6 +265,7 @@ export type UnprocessedType = {
|
|||
serverTimestamp?: number;
|
||||
decrypted?: string;
|
||||
urgent?: boolean;
|
||||
story?: boolean;
|
||||
};
|
||||
|
||||
export type UnprocessedUpdateType = {
|
||||
|
|
|
@ -3178,6 +3178,7 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
|||
serverTimestamp,
|
||||
decrypted,
|
||||
urgent,
|
||||
story,
|
||||
} = data;
|
||||
if (!id) {
|
||||
throw new Error('saveUnprocessedSync: id was falsey');
|
||||
|
@ -3204,7 +3205,8 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
|||
serverGuid,
|
||||
serverTimestamp,
|
||||
decrypted,
|
||||
urgent
|
||||
urgent,
|
||||
story
|
||||
) values (
|
||||
$id,
|
||||
$timestamp,
|
||||
|
@ -3218,7 +3220,8 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
|||
$serverGuid,
|
||||
$serverTimestamp,
|
||||
$decrypted,
|
||||
$urgent
|
||||
$urgent,
|
||||
$story
|
||||
);
|
||||
`
|
||||
).run({
|
||||
|
@ -3235,6 +3238,7 @@ function saveUnprocessedSync(data: UnprocessedType): string {
|
|||
serverTimestamp: serverTimestamp || null,
|
||||
decrypted: decrypted || null,
|
||||
urgent: urgent || !isBoolean(urgent) ? 1 : 0,
|
||||
story: story ? 1 : 0,
|
||||
});
|
||||
|
||||
return id;
|
||||
|
@ -3309,6 +3313,7 @@ async function getUnprocessedById(
|
|||
return {
|
||||
...row,
|
||||
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
|
||||
story: Boolean(row.story),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3370,6 +3375,7 @@ async function getAllUnprocessedAndIncrementAttempts(): Promise<
|
|||
.map(row => ({
|
||||
...row,
|
||||
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 updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
|
||||
import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos';
|
||||
import updateToSchemaVersion67 from './67-add-story-to-unprocessed';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -1947,6 +1948,7 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion64,
|
||||
updateToSchemaVersion65,
|
||||
updateToSchemaVersion66,
|
||||
updateToSchemaVersion67,
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
describe('rate-limit/story', function needsName() {
|
||||
describe('story/no-sender-key', function needsName() {
|
||||
this.timeout(durations.MINUTE);
|
||||
|
||||
let bootstrap: Bootstrap;
|
||||
|
@ -65,7 +65,7 @@ describe('rate-limit/story', function needsName() {
|
|||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('should request challenge and accept solution', async () => {
|
||||
it('should successfully send story', async () => {
|
||||
const {
|
||||
server,
|
||||
contactsWithoutProfileKey: contacts,
|
||||
|
@ -115,29 +115,6 @@ describe('rate-limit/story', function needsName() {
|
|||
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');
|
||||
await Promise.all(
|
||||
contacts.map(async contact => {
|
||||
|
|
|
@ -52,7 +52,6 @@ import { QualifiedAddress } from '../types/QualifiedAddress';
|
|||
import type { UUIDStringType } from '../types/UUID';
|
||||
import { UUID, UUIDKind } from '../types/UUID';
|
||||
import * as Errors from '../types/errors';
|
||||
import { isEnabled } from '../RemoteConfig';
|
||||
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
||||
|
@ -115,6 +114,8 @@ import { areArraysMatchingSets } from '../util/areArraysMatchingSets';
|
|||
import { generateBlurHash } from '../util/generateBlurHash';
|
||||
import { TEXT_ATTACHMENT } from '../types/MIME';
|
||||
import type { SendTypesType } from '../util/handleMessageSend';
|
||||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||
import { getStoriesBlocked } from '../types/Stories';
|
||||
|
||||
const GROUPV1_ID_LENGTH = 16;
|
||||
const GROUPV2_ID_LENGTH = 32;
|
||||
|
@ -394,6 +395,7 @@ export default class MessageReceiver
|
|||
serverGuid: decoded.serverGuid,
|
||||
serverTimestamp,
|
||||
urgent: isBoolean(decoded.urgent) ? decoded.urgent : true,
|
||||
story: decoded.story,
|
||||
};
|
||||
|
||||
// After this point, decoding errors are not the server's
|
||||
|
@ -777,6 +779,7 @@ export default class MessageReceiver
|
|||
serverTimestamp:
|
||||
item.serverTimestamp || decoded.serverTimestamp?.toNumber(),
|
||||
urgent: isBoolean(item.urgent) ? item.urgent : true,
|
||||
story: Boolean(item.story),
|
||||
};
|
||||
|
||||
const { decrypted } = item;
|
||||
|
@ -1043,6 +1046,7 @@ export default class MessageReceiver
|
|||
receivedAtCounter: envelope.receivedAtCounter,
|
||||
timestamp: envelope.timestamp,
|
||||
urgent: envelope.urgent,
|
||||
story: envelope.story,
|
||||
};
|
||||
this.decryptAndCacheBatcher.add({
|
||||
request,
|
||||
|
@ -1271,10 +1275,10 @@ export default class MessageReceiver
|
|||
envelope: UnsealedEnvelope,
|
||||
uuidKind: UUIDKind
|
||||
): Promise<DecryptResult> {
|
||||
const logId = getEnvelopeId(envelope);
|
||||
const logId = `MessageReceiver.decryptEnvelope(${getEnvelopeId(envelope)})`;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -1298,7 +1302,7 @@ export default class MessageReceiver
|
|||
);
|
||||
}
|
||||
|
||||
log.info(`MessageReceiver.decryptEnvelope(${logId})`);
|
||||
log.info(logId);
|
||||
const plaintext = await this.decrypt(
|
||||
stores,
|
||||
envelope,
|
||||
|
@ -1307,7 +1311,7 @@ export default class MessageReceiver
|
|||
);
|
||||
|
||||
if (!plaintext) {
|
||||
log.warn('MessageReceiver.decryptEnvelope: plaintext was falsey');
|
||||
log.warn(`${logId}: plaintext was falsey`);
|
||||
return { plaintext, envelope };
|
||||
}
|
||||
|
||||
|
@ -1331,6 +1335,53 @@ export default class MessageReceiver
|
|||
envelope,
|
||||
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) {
|
||||
|
@ -1359,8 +1410,7 @@ export default class MessageReceiver
|
|||
inProgressMessageType = '';
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'MessageReceiver.decryptEnvelope: ' +
|
||||
`Failed to process ${inProgressMessageType} ` +
|
||||
`${logId}: Failed to process ${inProgressMessageType} ` +
|
||||
`message: ${Errors.toLogFormat(error)}`
|
||||
);
|
||||
}
|
||||
|
@ -1371,9 +1421,7 @@ export default class MessageReceiver
|
|||
((envelope.source && this.isBlocked(envelope.source)) ||
|
||||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)))
|
||||
) {
|
||||
log.info(
|
||||
'MessageReceiver.decryptEnvelope: Dropping non-GV2 message from blocked sender'
|
||||
);
|
||||
log.info(`${logId}: Dropping non-GV2 message from blocked sender`);
|
||||
return { plaintext: undefined, envelope };
|
||||
}
|
||||
|
||||
|
@ -1900,16 +1948,19 @@ export default class MessageReceiver
|
|||
sentMessage?: ProcessedSent
|
||||
): Promise<void> {
|
||||
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);
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('MessageReceiver.handleStoryMessage', logId);
|
||||
|
||||
const attachments: Array<ProcessedAttachment> = [];
|
||||
|
||||
if (msg.fileAttachment) {
|
||||
const attachment = processAttachment(msg.fileAttachment);
|
||||
attachments.push(attachment);
|
||||
|
@ -2076,16 +2127,12 @@ export default class MessageReceiver
|
|||
const logId = getEnvelopeId(envelope);
|
||||
log.info('MessageReceiver.handleDataMessage', logId);
|
||||
|
||||
const isStoriesEnabled =
|
||||
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
|
||||
if (!isStoriesEnabled && msg.storyContext) {
|
||||
logUnexpectedUrgentValue(envelope, 'story');
|
||||
|
||||
if (getStoriesBlocked() && msg.storyContext) {
|
||||
log.info(
|
||||
`MessageReceiver.handleDataMessage/${logId}: Dropping incoming dataMessage with storyContext field`
|
||||
);
|
||||
this.removeFromCache(envelope);
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let p: Promise<void> = Promise.resolve();
|
||||
|
@ -2129,9 +2176,7 @@ export default class MessageReceiver
|
|||
|
||||
let type: SendTypesType = 'message';
|
||||
|
||||
if (msg.storyContext) {
|
||||
type = 'story';
|
||||
} else if (msg.body) {
|
||||
if (msg.storyContext || msg.body) {
|
||||
type = 'message';
|
||||
} else if (msg.reaction) {
|
||||
type = 'reaction';
|
||||
|
@ -2294,22 +2339,11 @@ export default class MessageReceiver
|
|||
return;
|
||||
}
|
||||
|
||||
const isStoriesEnabled =
|
||||
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
|
||||
if (content.storyMessage) {
|
||||
if (isStoriesEnabled) {
|
||||
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;
|
||||
}
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
|
||||
if (Bytes.isEmpty(content.senderKeyDistributionMessage)) {
|
||||
|
@ -2689,16 +2723,17 @@ export default class MessageReceiver
|
|||
const sentMessage = syncMessage.sent;
|
||||
|
||||
if (sentMessage.storyMessageRecipients && sentMessage.isRecipientUpdate) {
|
||||
if (window.Events.getHasStoriesDisabled()) {
|
||||
if (getStoriesBlocked()) {
|
||||
log.info(
|
||||
'MessageReceiver.handleSyncMessage: dropping story recipients update'
|
||||
'MessageReceiver.handleSyncMessage: dropping story recipients update',
|
||||
getEnvelopeId(envelope)
|
||||
);
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(
|
||||
'MessageReceiver.handleSyncMessage: handling storyMessageRecipients isRecipientUpdate sync message',
|
||||
'MessageReceiver.handleSyncMessage: handling story recipients update',
|
||||
getEnvelopeId(envelope)
|
||||
);
|
||||
const ev = new StoryRecipientUpdateEvent(
|
||||
|
|
|
@ -1255,6 +1255,7 @@ export default class MessageSender {
|
|||
groupId,
|
||||
options,
|
||||
urgent,
|
||||
story,
|
||||
}: Readonly<{
|
||||
timestamp: number;
|
||||
recipients: Array<string>;
|
||||
|
@ -1263,6 +1264,7 @@ export default class MessageSender {
|
|||
groupId: string | undefined;
|
||||
options?: SendOptionsType;
|
||||
urgent: boolean;
|
||||
story?: boolean;
|
||||
}>): Promise<CallbackResultType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (result: CallbackResultType) => {
|
||||
|
@ -1282,6 +1284,7 @@ export default class MessageSender {
|
|||
recipients,
|
||||
timestamp,
|
||||
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;
|
||||
groupId?: string;
|
||||
urgent?: boolean;
|
||||
story?: boolean;
|
||||
}>;
|
||||
|
||||
export type ProcessedAttachment = {
|
||||
|
|
|
@ -942,6 +942,7 @@ export type WebAPIType = {
|
|||
timestamp: number,
|
||||
options: {
|
||||
online?: boolean;
|
||||
story?: boolean;
|
||||
urgent?: boolean;
|
||||
}
|
||||
) => Promise<MultiRecipient200ResponseType>;
|
||||
|
@ -2122,14 +2123,13 @@ export function initialize({
|
|||
messages,
|
||||
timestamp,
|
||||
online: Boolean(online),
|
||||
story,
|
||||
urgent,
|
||||
};
|
||||
|
||||
await _ajax({
|
||||
call: 'messages',
|
||||
httpType: 'PUT',
|
||||
urlParameters: `/${destination}`,
|
||||
urlParameters: `/${destination}?story=${booleanToString(story)}`,
|
||||
jsonData,
|
||||
responseType: 'json',
|
||||
unauthenticated: true,
|
||||
|
@ -2151,14 +2151,13 @@ export function initialize({
|
|||
messages,
|
||||
timestamp,
|
||||
online: Boolean(online),
|
||||
story,
|
||||
urgent,
|
||||
};
|
||||
|
||||
await _ajax({
|
||||
call: 'messages',
|
||||
httpType: 'PUT',
|
||||
urlParameters: `/${destination}`,
|
||||
urlParameters: `/${destination}?story=${booleanToString(story)}`,
|
||||
jsonData,
|
||||
responseType: 'json',
|
||||
});
|
||||
|
@ -2175,20 +2174,23 @@ export function initialize({
|
|||
{
|
||||
online,
|
||||
urgent = true,
|
||||
story = false,
|
||||
}: {
|
||||
online?: boolean;
|
||||
story?: boolean;
|
||||
urgent?: boolean;
|
||||
}
|
||||
): Promise<MultiRecipient200ResponseType> {
|
||||
const onlineParam = `&online=${booleanToString(online)}`;
|
||||
const urgentParam = `&urgent=${booleanToString(urgent)}`;
|
||||
const storyParam = `&story=${booleanToString(story)}`;
|
||||
|
||||
const response = await _ajax({
|
||||
call: 'multiRecipient',
|
||||
httpType: 'PUT',
|
||||
contentType: 'application/vnd.signal-messenger.mrm',
|
||||
data,
|
||||
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}`,
|
||||
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`,
|
||||
responseType: 'json',
|
||||
unauthenticated: true,
|
||||
accessKey: Bytes.toBase64(accessKeys),
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { ReadStatus } from '../messages/MessageReadStatus';
|
|||
import type { SendStatus } from '../messages/MessageSendState';
|
||||
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
import { isEnabled } from '../RemoteConfig';
|
||||
|
||||
export type ReplyType = {
|
||||
author: Pick<
|
||||
|
@ -142,3 +143,9 @@ export enum HasStories {
|
|||
Read = 'Read',
|
||||
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,
|
||||
} from '../textsecure/SendMessage';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { getRandomBytes } from '../Crypto';
|
||||
import { getRandomBytes, getZeroes } from '../Crypto';
|
||||
import { getConversationMembers } from './getConversationMembers';
|
||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
||||
import { senderCertificateService } from '../services/senderCertificate';
|
||||
|
@ -24,14 +24,17 @@ const SEALED_SENDER = {
|
|||
};
|
||||
|
||||
export async function getSendOptionsForRecipients(
|
||||
recipients: ReadonlyArray<string>
|
||||
recipients: ReadonlyArray<string>,
|
||||
options?: { story?: boolean }
|
||||
): Promise<SendOptionsType> {
|
||||
const conversations = recipients
|
||||
.map(identifier => window.ConversationController.get(identifier))
|
||||
.filter(isNotNil);
|
||||
|
||||
const metadataList = await Promise.all(
|
||||
conversations.map(conversation => getSendOptions(conversation.attributes))
|
||||
conversations.map(conversation =>
|
||||
getSendOptions(conversation.attributes, options)
|
||||
)
|
||||
);
|
||||
|
||||
return metadataList.reduce(
|
||||
|
@ -58,9 +61,9 @@ export async function getSendOptionsForRecipients(
|
|||
|
||||
export async function getSendOptions(
|
||||
conversationAttrs: ConversationAttributesType,
|
||||
options: { syncMessage?: boolean } = {}
|
||||
options: { syncMessage?: boolean; story?: boolean } = {}
|
||||
): Promise<SendOptionsType> {
|
||||
const { syncMessage } = options;
|
||||
const { syncMessage, story } = options;
|
||||
|
||||
if (!isDirectConversation(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 (sealedSender === SEALED_SENDER.UNKNOWN) {
|
||||
if (sealedSender === SEALED_SENDER.UNKNOWN || story) {
|
||||
const identifierData = {
|
||||
accessKey: accessKey || Bytes.toBase64(getRandomBytes(16)),
|
||||
accessKey:
|
||||
accessKey ||
|
||||
(story
|
||||
? Bytes.toBase64(getZeroes(16))
|
||||
: Bytes.toBase64(getRandomBytes(16))),
|
||||
senderCertificate,
|
||||
};
|
||||
return {
|
||||
|
|
|
@ -38,6 +38,7 @@ import type {
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
import * as log from '../logging/log';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
|
||||
|
||||
const RETRY_LIMIT = 5;
|
||||
|
||||
|
@ -138,7 +139,8 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
|||
|
||||
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),
|
||||
logId,
|
||||
messageIds,
|
||||
|
@ -146,44 +148,35 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
|||
requesterUuid,
|
||||
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
|
||||
// the message was sent to.
|
||||
if (contentProto.storyMessage) {
|
||||
const { storyDistributionLists } = window.reduxStore.getState();
|
||||
const membersByListId = new Map<string, Set<string>>();
|
||||
storyDistributionLists.distributionLists.forEach(list => {
|
||||
membersByListId.set(list.id, new Set(list.memberUuids));
|
||||
// Assert that the requesting UUID is still part of a story distribution list that
|
||||
// the message was sent to, and add its sender key distribution message (SKDM).
|
||||
if (contentProto.storyMessage && !groupId) {
|
||||
contentProto = await checkDistributionListAndAddSKDM({
|
||||
confirm,
|
||||
contentProto,
|
||||
logId,
|
||||
messaging,
|
||||
requesterUuid,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
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();
|
||||
if (!contentProto) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const story = Boolean(contentProto.storyMessage);
|
||||
|
||||
const recipientConversation = window.ConversationController.getOrCreate(
|
||||
requesterUuid,
|
||||
'private'
|
||||
);
|
||||
const sendOptions = await getSendOptions(recipientConversation.attributes);
|
||||
const sendOptions = await getSendOptions(recipientConversation.attributes, {
|
||||
story,
|
||||
});
|
||||
const promise = messaging.sendMessageProtoAndWait({
|
||||
contentHint,
|
||||
groupId,
|
||||
|
@ -192,6 +185,7 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
|
|||
recipients: [requesterUuid],
|
||||
timestamp,
|
||||
urgent,
|
||||
story,
|
||||
});
|
||||
|
||||
await handleMessageSend(promise, {
|
||||
|
@ -427,6 +421,88 @@ async function getRetryConversation({
|
|||
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({
|
||||
contentProto,
|
||||
logId,
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { UUIDStringType } from '../types/UUID';
|
|||
import * as log from '../logging/log';
|
||||
import dataInterface from '../sql/Client';
|
||||
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 { SeenStatus } from '../MessageSeenStatus';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
|
@ -28,10 +28,17 @@ export async function sendStoryMessage(
|
|||
conversationIds: Array<string>,
|
||||
attachment: AttachmentType
|
||||
): Promise<void> {
|
||||
if (getStoriesBlocked()) {
|
||||
log.warn('stories.sendStoryMessage: stories disabled, returning early');
|
||||
return;
|
||||
}
|
||||
|
||||
const { messaging } = window.textsecure;
|
||||
|
||||
if (!messaging) {
|
||||
log.warn('stories.sendStoryMessage: messaging not available');
|
||||
log.warn(
|
||||
'stories.sendStoryMessage: messaging not available, returning early'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -99,6 +99,7 @@ export async function sendToGroup({
|
|||
sendOptions,
|
||||
sendTarget,
|
||||
sendType,
|
||||
story,
|
||||
urgent,
|
||||
}: {
|
||||
abortSignal?: AbortSignal;
|
||||
|
@ -109,6 +110,7 @@ export async function sendToGroup({
|
|||
sendOptions?: SendOptionsType;
|
||||
sendTarget: SenderKeyTargetType;
|
||||
sendType: SendTypesType;
|
||||
story?: boolean;
|
||||
urgent: boolean;
|
||||
}): Promise<CallbackResultType> {
|
||||
strictAssert(
|
||||
|
@ -141,6 +143,7 @@ export async function sendToGroup({
|
|||
sendOptions,
|
||||
sendTarget,
|
||||
sendType,
|
||||
story,
|
||||
timestamp,
|
||||
urgent,
|
||||
});
|
||||
|
@ -377,7 +380,7 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
// 4. Partition devices into sender key and non-sender key groups
|
||||
const [devicesForSenderKey, devicesForNormalSend] = partition(
|
||||
currentDevices,
|
||||
device => isValidSenderKeyRecipient(memberSet, device.identifier)
|
||||
device => isValidSenderKeyRecipient(memberSet, device.identifier, { story })
|
||||
);
|
||||
|
||||
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
|
||||
|
@ -513,13 +516,13 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
contentMessage: Proto.Content.encode(contentMessage).finish(),
|
||||
groupId,
|
||||
});
|
||||
const accessKeys = getXorOfAccessKeys(devicesForSenderKey);
|
||||
const accessKeys = getXorOfAccessKeys(devicesForSenderKey, { story });
|
||||
|
||||
const result = await window.textsecure.messaging.server.sendWithSenderKey(
|
||||
messageBuffer,
|
||||
accessKeys,
|
||||
timestamp,
|
||||
{ online, urgent }
|
||||
{ online, story, urgent }
|
||||
);
|
||||
|
||||
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 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) {
|
||||
throw new Error(`getXorOfAccessKeys: No accessKey for UUID ${uuid}`);
|
||||
}
|
||||
|
@ -1099,7 +1105,8 @@ async function encryptForSenderKey({
|
|||
|
||||
function isValidSenderKeyRecipient(
|
||||
members: Set<ConversationModel>,
|
||||
uuid: string
|
||||
uuid: string,
|
||||
{ story }: { story?: boolean } = {}
|
||||
): boolean {
|
||||
const memberConversation = window.ConversationController.get(uuid);
|
||||
if (!memberConversation) {
|
||||
|
@ -1121,7 +1128,7 @@ function isValidSenderKeyRecipient(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!getAccessKey(memberConversation.attributes)) {
|
||||
if (!getAccessKey(memberConversation.attributes, { story })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1247,10 +1254,15 @@ async function resetSenderKey(sendTarget: SenderKeyTargetType): Promise<void> {
|
|||
}
|
||||
|
||||
function getAccessKey(
|
||||
attributes: ConversationAttributesType
|
||||
attributes: ConversationAttributesType,
|
||||
{ story }: { story?: boolean }
|
||||
): string | undefined {
|
||||
const { sealedSender, accessKey } = attributes;
|
||||
|
||||
if (story) {
|
||||
return accessKey || ZERO_ACCESS_KEY;
|
||||
}
|
||||
|
||||
if (sealedSender === SEALED_SENDER.ENABLED) {
|
||||
return accessKey || undefined;
|
||||
}
|
||||
|
@ -1307,11 +1319,13 @@ async function fetchKeysForIdentifier(
|
|||
);
|
||||
|
||||
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(
|
||||
identifier,
|
||||
window.textsecure?.messaging?.server,
|
||||
devices,
|
||||
getAccessKey(emptyConversation.attributes)
|
||||
getAccessKey(emptyConversation.attributes, { story: false })
|
||||
);
|
||||
if (accessKeyFailed) {
|
||||
log.info(
|
||||
|
|
|
@ -1975,10 +1975,10 @@
|
|||
node-gyp-build "^4.2.3"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@signalapp/mock-server@2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.10.0.tgz#a27246e7b912caebc0bef628303e11689bf9b74c"
|
||||
integrity sha512-kHos3n8lNBhivUecEFG4g1rvYpJ6oPgzKMOsaI+vN8R1R4Pc63WXxrLsxqAI2QmAngD+nmOgbjwAvKyH4MN0+w==
|
||||
"@signalapp/mock-server@2.11.0":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.11.0.tgz#fe5f6229c4a5c28b3591e986a1622218452c5112"
|
||||
integrity sha512-m23XZ8lrBn0u+zakxkKG5SezyUg6fnWwZewFF28sHNL7fQDVPHJkFCJZgE9XJwHBDM7TYz9ca/ucReW4GIPHoQ==
|
||||
dependencies:
|
||||
"@signalapp/libsignal-client" "^0.20.0"
|
||||
debug "^4.3.2"
|
||||
|
|
Loading…
Reference in a new issue