mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-17 15:23:36 +01:00
Polls: Support sending polls in 1:1 conversations
Co-authored-by: yash-signal <yash@signal.org>
This commit is contained in:
@@ -50,6 +50,8 @@ const SemverKeys = [
|
||||
'desktop.keyTransparency.prod',
|
||||
'desktop.binaryServiceId.beta',
|
||||
'desktop.binaryServiceId.prod',
|
||||
'desktop.pollSend1to1.beta',
|
||||
'desktop.pollSend1to1.prod',
|
||||
] as const;
|
||||
|
||||
export type SemverKeyType = ArrayValues<typeof SemverKeys>;
|
||||
|
||||
@@ -79,6 +79,7 @@ export default {
|
||||
i18n,
|
||||
isDisabled: false,
|
||||
isFormattingEnabled: true,
|
||||
isPollSend1to1Enabled: true,
|
||||
messageCompositionId: '456',
|
||||
sendEditedMessage: action('sendEditedMessage'),
|
||||
sendMultiMediaMessage: action('sendMultiMediaMessage'),
|
||||
|
||||
@@ -125,6 +125,7 @@ export type OwnProps = Readonly<{
|
||||
isFormattingEnabled: boolean;
|
||||
isGroupV1AndDisabled: boolean | null;
|
||||
isMissingMandatoryProfileSharing: boolean | null;
|
||||
isPollSend1to1Enabled: boolean;
|
||||
isSignalConversation: boolean | null;
|
||||
isActive: boolean;
|
||||
lastEditableMessageId: string | null;
|
||||
@@ -246,6 +247,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
i18n,
|
||||
imageToBlurHash,
|
||||
isDisabled,
|
||||
isPollSend1to1Enabled,
|
||||
isSignalConversation,
|
||||
isMuted,
|
||||
isActive,
|
||||
@@ -776,7 +778,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
<AxoDropdownMenu.Item symbol="file" onSelect={launchFilePicker}>
|
||||
{i18n('icu:CompositionArea__AttachMenu__File')}
|
||||
</AxoDropdownMenu.Item>
|
||||
{conversationType === 'group' && (
|
||||
{(conversationType === 'group' || isPollSend1to1Enabled) && (
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="poll"
|
||||
onSelect={handleOpenPollModal}
|
||||
|
||||
@@ -26,8 +26,6 @@ import type {
|
||||
} from '../conversationJobQueue.preload.js';
|
||||
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds.dom.js';
|
||||
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { getMessageById } from '../../messages/getMessageById.preload.js';
|
||||
import { isNotNil } from '../../util/isNotNil.std.js';
|
||||
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
|
||||
@@ -38,6 +36,7 @@ import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import type { ServiceIdString } from '../../types/ServiceId.std.js';
|
||||
import { isStory } from '../../messages/helpers.std.js';
|
||||
import { sendToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
|
||||
const { isNumber } = lodash;
|
||||
|
||||
@@ -157,37 +156,10 @@ export async function sendDeleteForEveryone(
|
||||
);
|
||||
await updateMessageWithSuccessfulSends(message);
|
||||
} else if (isDirectConversation(conversation.attributes)) {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
void updateMessageWithFailure(
|
||||
message,
|
||||
[new Error('Message request was not accepted')],
|
||||
log
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
void updateMessageWithFailure(
|
||||
message,
|
||||
[new Error('Contact no longer has a Signal account')],
|
||||
log
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
void updateMessageWithFailure(
|
||||
message,
|
||||
[new Error('Contact is blocked')],
|
||||
log
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
log.info(refusal.logLine);
|
||||
void updateMessageWithFailure(message, [refusal.error], log);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import type {
|
||||
} from '../conversationJobQueue.preload.js';
|
||||
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds.dom.js';
|
||||
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { getMessageById } from '../../messages/getMessageById.preload.js';
|
||||
import { isNotNil } from '../../util/isNotNil.std.js';
|
||||
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
|
||||
@@ -32,6 +30,7 @@ import { SendMessageProtoError } from '../../textsecure/Errors.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import { isStory } from '../../messages/helpers.std.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
|
||||
export async function sendDeleteStoryForEveryone(
|
||||
ourConversation: ConversationModel,
|
||||
@@ -109,40 +108,10 @@ export async function sendDeleteStoryForEveryone(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
log.info(
|
||||
`${logId}: conversation ${conversation.idForLogging()} ` +
|
||||
'is not accepted; refusing to send'
|
||||
);
|
||||
void updateMessageWithFailure(
|
||||
message,
|
||||
[new Error('Message request was not accepted')],
|
||||
log
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.info(
|
||||
`${logId}: conversation ${conversation.idForLogging()} ` +
|
||||
'is unregistered; refusing to send'
|
||||
);
|
||||
void updateMessageWithFailure(
|
||||
message,
|
||||
[new Error('Contact no longer has a Signal account')],
|
||||
log
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
log.info(
|
||||
`${logId}: conversation ${conversation.idForLogging()} ` +
|
||||
'is blocked; refusing to send'
|
||||
);
|
||||
void updateMessageWithFailure(
|
||||
message,
|
||||
[new Error('Contact is blocked')],
|
||||
log
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
log.info(`${logId}: ${refusal.logLine}`);
|
||||
void updateMessageWithFailure(message, [refusal.error], log);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,8 @@ import type {
|
||||
ConversationQueueJobBundle,
|
||||
} from '../conversationJobQueue.preload.js';
|
||||
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { DurationInSeconds } from '../../util/durations/index.std.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
|
||||
export async function sendDirectExpirationTimerUpdate(
|
||||
conversation: ConversationModel,
|
||||
@@ -119,22 +118,9 @@ export async function sendDirectExpirationTimerUpdate(
|
||||
{ messageIds: [], sendType }
|
||||
);
|
||||
} else if (isDirectConversation(conversation.attributes)) {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
log.info(refusal.logLine);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,6 @@ import type { QuotedMessageType } from '../../model-types.d.ts';
|
||||
|
||||
import { handleMultipleSendErrors } from './handleMultipleSendErrors.std.js';
|
||||
import { ourProfileKeyService } from '../../services/ourProfileKey.std.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { sendToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import type { DurationInSeconds } from '../../util/durations/index.std.js';
|
||||
import type { ServiceIdString } from '../../types/ServiceId.std.js';
|
||||
@@ -81,6 +79,7 @@ import { getMessageIdForLogging } from '../../util/idForLogging.preload.js';
|
||||
import { send, sendSyncMessageOnly } from '../../messages/send.preload.js';
|
||||
import type { SignalService } from '../../protobuf/index.std.js';
|
||||
import { eraseMessageContents } from '../../util/cleanup.preload.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
|
||||
const { isNumber } = lodash;
|
||||
|
||||
@@ -383,35 +382,12 @@ export async function sendNormalMessage(
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
log.info(refusal.logLine);
|
||||
void markMessageFailed({
|
||||
message,
|
||||
errors: [new Error('Message request was not accepted')],
|
||||
targetTimestamp,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
void markMessageFailed({
|
||||
message,
|
||||
errors: [new Error('Contact no longer has a Signal account')],
|
||||
targetTimestamp,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
void markMessageFailed({
|
||||
message,
|
||||
errors: [new Error('Contact is blocked')],
|
||||
errors: [refusal.error],
|
||||
targetTimestamp,
|
||||
});
|
||||
return;
|
||||
@@ -438,6 +414,7 @@ export async function sendNormalMessage(
|
||||
targetTimestampForEdit: editedMessageTimestamp
|
||||
? targetOfThisEditTimestamp
|
||||
: undefined,
|
||||
pollCreate: poll,
|
||||
timestamp: targetTimestamp,
|
||||
},
|
||||
contentHint: ContentHint.Resendable,
|
||||
|
||||
@@ -15,7 +15,12 @@ import { sendContentMessageToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import { getMessageById } from '../../messages/getMessageById.preload.js';
|
||||
import { ourProfileKeyService } from '../../services/ourProfileKey.std.js';
|
||||
import { getSendOptions } from '../../util/getSendOptions.preload.js';
|
||||
import { isGroupV2 } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import {
|
||||
isGroupV2,
|
||||
isDirectConversation,
|
||||
isMe,
|
||||
} from '../../util/whatTypeOfConversation.dom.js';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.js';
|
||||
import {
|
||||
handleMultipleSendErrors,
|
||||
maybeExpandErrors,
|
||||
@@ -26,6 +31,9 @@ import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { DataWriter } from '../../sql/Client.preload.js';
|
||||
import { cleanupMessages } from '../../util/cleanup.preload.js';
|
||||
import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
|
||||
const { isNumber } = lodash;
|
||||
|
||||
@@ -57,11 +65,6 @@ export async function sendPollTerminate(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGroupV2(conversation.attributes)) {
|
||||
jobLog.error(`${logId}: Non-GroupV2 conversation. Failing job.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldContinue) {
|
||||
jobLog.info(`${logId}: Ran out of time. Giving up on sending`);
|
||||
await markTerminateFailed(pollMessage, jobLog);
|
||||
@@ -70,86 +73,195 @@ export async function sendPollTerminate(
|
||||
|
||||
const recipients = getRecipients(conversation.attributes);
|
||||
|
||||
await conversation.queueJob(
|
||||
'conversationQueue/sendPollTerminate',
|
||||
async abortSignal => {
|
||||
const profileKey = conversation.get('profileSharing')
|
||||
? await ourProfileKeyService.get()
|
||||
: undefined;
|
||||
|
||||
const sendOptions = await getSendOptions(conversation.attributes);
|
||||
const timestamp = Date.now();
|
||||
const expireTimer = conversation.get('expireTimer');
|
||||
|
||||
try {
|
||||
const isGroupV2Conversation = isGroupV2(conversation.attributes);
|
||||
const shouldSendSyncOnly =
|
||||
(isDirectConversation(conversation.attributes) &&
|
||||
isMe(conversation.attributes)) ||
|
||||
(isGroupV2Conversation && recipients.length === 0);
|
||||
|
||||
if (shouldSendSyncOnly) {
|
||||
jobLog.info(
|
||||
`${logId}: Sending poll terminate for poll timestamp ${targetTimestamp}`
|
||||
`${logId}: Sending poll terminate for poll timestamp ${targetTimestamp} (sync only)`
|
||||
);
|
||||
|
||||
const profileKey = conversation.get('profileSharing')
|
||||
? await ourProfileKeyService.get()
|
||||
const groupV2Info = isGroupV2Conversation
|
||||
? conversation.getGroupV2Info({
|
||||
members: recipients,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const sendOptions = await getSendOptions(conversation.attributes);
|
||||
|
||||
try {
|
||||
if (isGroupV2(conversation.attributes) && !isNumber(revision)) {
|
||||
jobLog.error('No revision provided, but conversation is GroupV2');
|
||||
}
|
||||
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipients,
|
||||
});
|
||||
if (groupV2Info && isNumber(revision)) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
strictAssert(groupV2Info, 'could not get group info from conversation');
|
||||
|
||||
const timestamp = Date.now();
|
||||
const expireTimer = conversation.get('expireTimer');
|
||||
|
||||
const contentMessage = await messaging.getPollTerminateContentMessage({
|
||||
groupV2: groupV2Info,
|
||||
timestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollTerminate: {
|
||||
targetTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error('sendPollTerminate was aborted');
|
||||
}
|
||||
|
||||
await wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId,
|
||||
messageIds: [pollMessageId],
|
||||
send: async () =>
|
||||
sendContentMessageToGroup({
|
||||
contentHint: ContentHint.Resendable,
|
||||
contentMessage,
|
||||
messageId: pollMessageId,
|
||||
recipients,
|
||||
sendOptions,
|
||||
sendTarget: conversation.toSenderKeyTarget(),
|
||||
sendType: 'pollTerminate',
|
||||
timestamp,
|
||||
urgent: true,
|
||||
}),
|
||||
sendType: 'pollTerminate',
|
||||
timestamp,
|
||||
expirationStartTimestamp: null,
|
||||
});
|
||||
|
||||
await markTerminateSuccess(pollMessage, jobLog);
|
||||
} catch (error: unknown) {
|
||||
const errors = maybeExpandErrors(error);
|
||||
await handleMultipleSendErrors({
|
||||
errors,
|
||||
isFinalAttempt,
|
||||
log: jobLog,
|
||||
markFailed: () => markTerminateFailed(pollMessage, jobLog),
|
||||
timeRemaining,
|
||||
toThrow: error,
|
||||
});
|
||||
if (isGroupV2Conversation) {
|
||||
strictAssert(
|
||||
groupV2Info,
|
||||
`${logId}: missing groupV2 info for sync-only poll terminate`
|
||||
);
|
||||
}
|
||||
|
||||
const dataMessage = messaging.createDataMessageProtoForPollTerminate({
|
||||
groupV2: groupV2Info,
|
||||
timestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollTerminate: {
|
||||
targetTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
await handleMessageSend(
|
||||
messaging.sendSyncMessage({
|
||||
encodedDataMessage: Proto.DataMessage.encode(dataMessage).finish(),
|
||||
destinationE164: conversation.get('e164'),
|
||||
destinationServiceId: conversation.getServiceId(),
|
||||
expirationStartTimestamp: null,
|
||||
options: sendOptions,
|
||||
timestamp,
|
||||
urgent: false,
|
||||
}),
|
||||
{ messageIds: [pollMessageId], sendType: 'pollTerminate' }
|
||||
);
|
||||
|
||||
await markTerminateSuccess(pollMessage, jobLog);
|
||||
} else if (isDirectConversation(conversation.attributes)) {
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
jobLog.info(`${logId}: ${refusal.logLine}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientServiceId = recipients[0];
|
||||
|
||||
jobLog.info(
|
||||
`${logId}: Sending direct poll terminate for poll timestamp ${targetTimestamp}`
|
||||
);
|
||||
|
||||
const contentMessage = await messaging.getPollTerminateContentMessage({
|
||||
groupV2: undefined,
|
||||
timestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollTerminate: {
|
||||
targetTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
addPniSignatureMessageToProto({
|
||||
conversation,
|
||||
proto: contentMessage,
|
||||
reason: `sendPollTerminate(${timestamp})`,
|
||||
});
|
||||
|
||||
await wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId,
|
||||
messageIds: [pollMessageId],
|
||||
send: async () =>
|
||||
messaging.sendMessageProtoAndWait({
|
||||
timestamp,
|
||||
recipients: [recipientServiceId],
|
||||
proto: contentMessage,
|
||||
contentHint: ContentHint.Resendable,
|
||||
groupId: undefined,
|
||||
options: sendOptions,
|
||||
urgent: true,
|
||||
}),
|
||||
sendType: 'pollTerminate',
|
||||
timestamp,
|
||||
expirationStartTimestamp: null,
|
||||
});
|
||||
|
||||
await markTerminateSuccess(pollMessage, jobLog);
|
||||
} else {
|
||||
strictAssert(
|
||||
isGroupV2Conversation,
|
||||
`${logId}: expected GroupV2 conversation when not direct`
|
||||
);
|
||||
|
||||
await conversation.queueJob(
|
||||
'conversationQueue/sendPollTerminate',
|
||||
async abortSignal => {
|
||||
jobLog.info(
|
||||
`${logId}: Sending group poll terminate for poll timestamp ${targetTimestamp}`
|
||||
);
|
||||
|
||||
if (!isNumber(revision)) {
|
||||
jobLog.error('No revision provided, but conversation is GroupV2');
|
||||
}
|
||||
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipients,
|
||||
});
|
||||
if (groupV2Info && isNumber(revision)) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
strictAssert(
|
||||
groupV2Info,
|
||||
'could not get group info from conversation'
|
||||
);
|
||||
|
||||
const contentMessage = await messaging.getPollTerminateContentMessage(
|
||||
{
|
||||
groupV2: groupV2Info,
|
||||
timestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollTerminate: {
|
||||
targetTimestamp,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error('sendPollTerminate was aborted');
|
||||
}
|
||||
|
||||
await wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId,
|
||||
messageIds: [pollMessageId],
|
||||
send: async () =>
|
||||
sendContentMessageToGroup({
|
||||
contentHint: ContentHint.Resendable,
|
||||
contentMessage,
|
||||
messageId: pollMessageId,
|
||||
recipients,
|
||||
sendOptions,
|
||||
sendTarget: conversation.toSenderKeyTarget(),
|
||||
sendType: 'pollTerminate',
|
||||
timestamp,
|
||||
urgent: true,
|
||||
}),
|
||||
sendType: 'pollTerminate',
|
||||
timestamp,
|
||||
expirationStartTimestamp: null,
|
||||
});
|
||||
|
||||
await markTerminateSuccess(pollMessage, jobLog);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errors = maybeExpandErrors(error);
|
||||
await handleMultipleSendErrors({
|
||||
errors,
|
||||
isFinalAttempt,
|
||||
log: jobLog,
|
||||
markFailed: () => markTerminateFailed(pollMessage, jobLog),
|
||||
timeRemaining,
|
||||
toThrow: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function markTerminateSuccess(
|
||||
|
||||
@@ -28,10 +28,9 @@ import * as pollVoteUtil from '../../polls/util.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { getSendRecipientLists } from './getSendRecipientLists.dom.js';
|
||||
import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
|
||||
import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
|
||||
export async function sendPollVote(
|
||||
conversation: ConversationModel,
|
||||
@@ -214,22 +213,9 @@ export async function sendPollVote(
|
||||
let promise: Promise<CallbackResultType>;
|
||||
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
jobLog.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
jobLog.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
jobLog.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
jobLog.info(refusal.logLine);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,13 +35,12 @@ import type {
|
||||
ConversationQueueJobBundle,
|
||||
ReactionJobData,
|
||||
} from '../conversationJobQueue.preload.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { sendToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import { hydrateStoryContext } from '../../util/hydrateStoryContext.preload.js';
|
||||
import { send, sendSyncMessageOnly } from '../../messages/send.preload.js';
|
||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
import { getSendRecipientLists } from './getSendRecipientLists.dom.js';
|
||||
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
|
||||
|
||||
const { isNumber } = lodash;
|
||||
|
||||
@@ -223,24 +222,9 @@ export async function sendReaction(
|
||||
|
||||
let promise: Promise<CallbackResultType>;
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
markReactionFailed(message, pendingReaction);
|
||||
return;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
markReactionFailed(message, pendingReaction);
|
||||
return;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
log.info(refusal.logLine);
|
||||
markReactionFailed(message, pendingReaction);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,15 @@ import type { ConversationModel } from '../../models/conversations.preload.js';
|
||||
import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import { getRecipients } from '../../util/getRecipients.dom.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation.dom.js';
|
||||
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds.dom.js';
|
||||
|
||||
type ConversationForDirectSendType = Pick<
|
||||
ConversationModel,
|
||||
'attributes' | 'isBlocked' | 'idForLogging'
|
||||
>;
|
||||
|
||||
export function shouldSendToConversation(
|
||||
conversation: ConversationModel,
|
||||
log: LoggerType
|
||||
@@ -45,3 +51,48 @@ export function shouldSendToConversation(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type DirectConversationSendRefusalType = Readonly<{
|
||||
logLine: string;
|
||||
error: Error;
|
||||
}>;
|
||||
|
||||
export type ShouldSendToDirectConversationResult =
|
||||
| readonly [ok: true, refusal: undefined]
|
||||
| readonly [ok: false, refusal: DirectConversationSendRefusalType];
|
||||
|
||||
export function shouldSendToDirectConversation(
|
||||
conversation: ConversationForDirectSendType
|
||||
): ShouldSendToDirectConversationResult {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
return [
|
||||
false,
|
||||
{
|
||||
logLine: `conversation ${conversation.idForLogging()} is not accepted; refusing to send`,
|
||||
error: new Error('Message request was not accepted'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
return [
|
||||
false,
|
||||
{
|
||||
logLine: `conversation ${conversation.idForLogging()} is unregistered; refusing to send`,
|
||||
error: new Error('Contact no longer has a Signal account'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (conversation.isBlocked()) {
|
||||
return [
|
||||
false,
|
||||
{
|
||||
logLine: `conversation ${conversation.idForLogging()} is blocked; refusing to send`,
|
||||
error: new Error('Contact is blocked'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [true, undefined];
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@ import {
|
||||
handleMultipleSendErrors,
|
||||
maybeExpandErrors,
|
||||
} from './helpers/handleMultipleSendErrors.std.js';
|
||||
import { isConversationUnregistered } from '../util/isConversationUnregistered.dom.js';
|
||||
import { isConversationAccepted } from '../util/isConversationAccepted.preload.js';
|
||||
import { parseUnknown } from '../util/schemas.std.js';
|
||||
import { shouldSendToDirectConversation } from './helpers/shouldSendToConversation.preload.js';
|
||||
|
||||
const { isBoolean } = lodash;
|
||||
|
||||
@@ -90,22 +89,9 @@ export class SingleProtoJobQueue extends JobQueue<SingleProtoJobData> {
|
||||
throw new Error(`Failed to get conversation for serviceId ${serviceId}`);
|
||||
}
|
||||
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(conversation);
|
||||
if (!ok) {
|
||||
log.info(refusal.logLine);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
PollSource,
|
||||
type PollTerminateAttributesType,
|
||||
} from '../messageModifiers/Polls.preload.js';
|
||||
import { isGroup } from '../util/whatTypeOfConversation.dom.js';
|
||||
import { isGroupV1 } from '../util/whatTypeOfConversation.dom.js';
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
|
||||
@@ -33,10 +33,12 @@ export async function enqueuePollTerminateForSend({
|
||||
conversation,
|
||||
'enqueuePollTerminateForSend: No conversation extracted from target message'
|
||||
);
|
||||
strictAssert(
|
||||
isGroup(conversation.attributes),
|
||||
'enqueuePollTerminateForSend: conversation must be a group'
|
||||
);
|
||||
if (isGroupV1(conversation.attributes)) {
|
||||
log.info(
|
||||
'enqueuePollTerminateForSend: refusing to send poll terminate to GroupV1'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const ourId = window.ConversationController.getOurConversationIdOrThrow();
|
||||
const timestamp = Date.now();
|
||||
|
||||
@@ -36,6 +36,7 @@ import { getSharedGroupNames } from '../../util/sharedGroupNames.dom.js';
|
||||
import {
|
||||
getDefaultConversationColor,
|
||||
getEmojiSkinToneDefault,
|
||||
getItems,
|
||||
getTextFormattingEnabled,
|
||||
} from '../selectors/items.dom.js';
|
||||
import { canForward, getPropsForQuote } from '../selectors/message.preload.js';
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
getPlatform,
|
||||
getTheme,
|
||||
getUserConversationId,
|
||||
getVersion,
|
||||
} from '../selectors/user.std.js';
|
||||
import { SmartCompositionRecording } from './CompositionRecording.preload.js';
|
||||
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft.preload.js';
|
||||
@@ -60,6 +62,7 @@ import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import { isConversationMuted } from '../../util/isConversationMuted.std.js';
|
||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
import { useNavActions } from '../ducks/nav.std.js';
|
||||
import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js';
|
||||
|
||||
function renderSmartCompositionRecording() {
|
||||
return <SmartCompositionRecording />;
|
||||
@@ -86,6 +89,8 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
const selectedMessageIds = useSelector(getSelectedMessageIds);
|
||||
const messageLookup = useSelector(getMessages);
|
||||
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
|
||||
const items = useSelector(getItems);
|
||||
const version = useSelector(getVersion);
|
||||
const lastEditableMessageId = useSelector(getLastEditableMessageId);
|
||||
const platform = useSelector(getPlatform);
|
||||
const shouldHidePopovers = useSelector(getHasPanelOpen);
|
||||
@@ -243,6 +248,12 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
i18n={i18n}
|
||||
isDisabled={isDisabled}
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isPollSend1to1Enabled={isFeaturedEnabledSelector({
|
||||
betaKey: 'desktop.pollSend1to1.beta',
|
||||
prodKey: 'desktop.pollSend1to1.prod',
|
||||
currentVersion: version,
|
||||
remoteConfig: items.remoteConfig,
|
||||
})}
|
||||
isActive={isActive}
|
||||
lastEditableMessageId={lastEditableMessageId ?? null}
|
||||
messageCompositionId={messageCompositionId}
|
||||
|
||||
@@ -255,9 +255,12 @@ export type PollVoteBuildOptions = Required<
|
||||
>;
|
||||
|
||||
export type PollTerminateBuildOptions = Required<
|
||||
Pick<MessageOptionsType, 'groupV2' | 'timestamp' | 'pollTerminate'>
|
||||
Pick<MessageOptionsType, 'timestamp' | 'pollTerminate'>
|
||||
> &
|
||||
Pick<MessageOptionsType, 'profileKey' | 'expireTimer' | 'expireTimerVersion'>;
|
||||
Pick<
|
||||
MessageOptionsType,
|
||||
'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion'
|
||||
>;
|
||||
|
||||
class Message {
|
||||
attachments: ReadonlyArray<Proto.IAttachmentPointer>;
|
||||
@@ -943,10 +946,12 @@ export class MessageSender {
|
||||
const dataMessage = new Proto.DataMessage();
|
||||
dataMessage.timestamp = Long.fromNumber(timestamp);
|
||||
|
||||
const groupContext = new Proto.GroupContextV2();
|
||||
groupContext.masterKey = groupV2.masterKey;
|
||||
groupContext.revision = groupV2.revision;
|
||||
dataMessage.groupV2 = groupContext;
|
||||
if (groupV2) {
|
||||
const groupContext = new Proto.GroupContextV2();
|
||||
groupContext.masterKey = groupV2.masterKey;
|
||||
groupContext.revision = groupV2.revision;
|
||||
dataMessage.groupV2 = groupContext;
|
||||
}
|
||||
|
||||
if (typeof expireTimer !== 'undefined') {
|
||||
dataMessage.expireTimer = expireTimer;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../environment.std.js';
|
||||
import * as RemoteConfig from '../RemoteConfig.dom.js';
|
||||
import { isAlpha, isBeta, isProduction } from '../util/version.std.js';
|
||||
import { isFeaturedEnabledNoRedux } from '../util/isFeatureEnabled.dom.js';
|
||||
import type { SendStateByConversationId } from '../messages/MessageSendState.std.js';
|
||||
import { aciSchema } from './ServiceId.std.js';
|
||||
import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js';
|
||||
@@ -173,3 +174,10 @@ export function isPollSendEnabled(): boolean {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isPollSend1to1Enabled(): boolean {
|
||||
return isFeaturedEnabledNoRedux({
|
||||
betaKey: 'desktop.pollSend1to1.beta',
|
||||
prodKey: 'desktop.pollSend1to1.prod',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationModel } from '../models/conversations.preload.js';
|
||||
import { isGroupV2 } from './whatTypeOfConversation.dom.js';
|
||||
import { isPollSendEnabled, type PollCreateType } from '../types/Polls.dom.js';
|
||||
import { isDirectConversation } from './whatTypeOfConversation.dom.js';
|
||||
import {
|
||||
isPollSend1to1Enabled,
|
||||
isPollSendEnabled,
|
||||
type PollCreateType,
|
||||
} from '../types/Polls.dom.js';
|
||||
|
||||
export async function enqueuePollCreateForSend(
|
||||
conversation: ConversationModel,
|
||||
@@ -13,9 +17,12 @@ export async function enqueuePollCreateForSend(
|
||||
throw new Error('enqueuePollCreateForSend: poll sending is not enabled');
|
||||
}
|
||||
|
||||
if (!isGroupV2(conversation.attributes)) {
|
||||
if (
|
||||
isDirectConversation(conversation.attributes) &&
|
||||
!isPollSend1to1Enabled()
|
||||
) {
|
||||
throw new Error(
|
||||
'enqueuePollCreateForSend: polls are group-only. Conversation is not GroupV2.'
|
||||
'enqueuePollCreateForSend: 1:1 poll sending is not enabled'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,13 @@ import type { Receipt } from '../types/Receipt.std.js';
|
||||
import { ReceiptType } from '../types/Receipt.std.js';
|
||||
import { getSendOptions } from './getSendOptions.preload.js';
|
||||
import { handleMessageSend } from './handleMessageSend.preload.js';
|
||||
import { isConversationAccepted } from './isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from './isConversationUnregistered.dom.js';
|
||||
import { missingCaseError } from './missingCaseError.std.js';
|
||||
import type { ConversationModel } from '../models/conversations.preload.js';
|
||||
import { mapEmplace } from './mapEmplace.std.js';
|
||||
import { isSignalConversation } from './isSignalConversation.dom.js';
|
||||
import { messageSender } from '../textsecure/SendMessage.preload.js';
|
||||
import { itemStorage } from '../textsecure/Storage.preload.js';
|
||||
import { shouldSendToDirectConversation } from '../jobs/helpers/shouldSendToConversation.preload.js';
|
||||
|
||||
const { chunk, map } = lodash;
|
||||
|
||||
@@ -106,22 +105,9 @@ export async function sendReceipts({
|
||||
Array.from(allGroups.values(), async group => {
|
||||
const { conversationId, sender, receipts: receiptsForSender } = group;
|
||||
|
||||
if (!isConversationAccepted(sender.attributes)) {
|
||||
log.info(
|
||||
`conversation ${sender.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isConversationUnregistered(sender.attributes)) {
|
||||
log.info(
|
||||
`conversation ${sender.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sender.isBlocked()) {
|
||||
log.info(
|
||||
`conversation ${sender.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
const [ok, refusal] = shouldSendToDirectConversation(sender);
|
||||
if (!ok) {
|
||||
log.info(refusal.logLine);
|
||||
return;
|
||||
}
|
||||
if (isSignalConversation(sender.attributes)) {
|
||||
|
||||
Reference in New Issue
Block a user