diff --git a/ts/components/PollCreateModal.dom.tsx b/ts/components/PollCreateModal.dom.tsx index 8420d1278e..4ef1be04a6 100644 --- a/ts/components/PollCreateModal.dom.tsx +++ b/ts/components/PollCreateModal.dom.tsx @@ -18,7 +18,8 @@ import { getEmojiVariantByKey } from './fun/data/emojis.std.js'; import { strictAssert } from '../util/assert.std.js'; import { type PollCreateType, - POLL_QUESTION_MAX_LENGTH, + POLL_QUESTION_MAX_LENGTH_SEND, + POLL_OPTION_MAX_LENGTH, POLL_OPTIONS_MIN_COUNT, POLL_OPTIONS_MAX_COUNT, } from '../types/Polls.dom.js'; @@ -188,7 +189,7 @@ export function PollCreateModal({ } // Don't insert if it would exceed the max grapheme length - if (countGraphemes(newValue) > POLL_QUESTION_MAX_LENGTH) { + if (countGraphemes(newValue) > POLL_OPTION_MAX_LENGTH) { return opt; // Return unchanged } @@ -314,8 +315,8 @@ export function PollCreateModal({ value={question} onChange={handleQuestionChange} placeholder={i18n('icu:PollCreateModal__questionPlaceholder')} - maxLengthCount={POLL_QUESTION_MAX_LENGTH} - whenToShowRemainingCount={POLL_QUESTION_MAX_LENGTH - 30} + maxLengthCount={POLL_QUESTION_MAX_LENGTH_SEND} + whenToShowRemainingCount={POLL_QUESTION_MAX_LENGTH_SEND - 30} aria-invalid={validationErrors.question || undefined} aria-errormessage={ validationErrors.question ? 'poll-question-error' : undefined @@ -340,8 +341,8 @@ export function PollCreateModal({ placeholder={i18n('icu:PollCreateModal__optionPlaceholder', { number: String(index + 1), })} - maxLengthCount={POLL_QUESTION_MAX_LENGTH} - whenToShowRemainingCount={POLL_QUESTION_MAX_LENGTH - 30} + maxLengthCount={POLL_OPTION_MAX_LENGTH} + whenToShowRemainingCount={POLL_OPTION_MAX_LENGTH - 30} aria-invalid={validationErrors.options || undefined} aria-errormessage={ validationErrors.options ? 'poll-options-error' : undefined diff --git a/ts/jobs/helpers/sendPollVote.preload.ts b/ts/jobs/helpers/sendPollVote.preload.ts index 2b7dc821bd..c5048d1f4e 100644 --- a/ts/jobs/helpers/sendPollVote.preload.ts +++ b/ts/jobs/helpers/sendPollVote.preload.ts @@ -3,7 +3,6 @@ import { ContentHint } from '@signalapp/libsignal-client'; import * as Errors from '../../types/errors.std.js'; -import { isGroupV2 } from '../../util/whatTypeOfConversation.dom.js'; import { getSendOptions } from '../../util/getSendOptions.preload.js'; import { handleMessageSend } from '../../util/handleMessageSend.preload.js'; import { sendContentMessageToGroup } from '../../util/sendToGroup.preload.js'; @@ -28,6 +27,11 @@ import type { 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'; export async function sendPollVote( conversation: ConversationModel, @@ -52,10 +56,6 @@ export async function sendPollVote( return; } - if (!isGroupV2(conversation.attributes)) { - jobLog.error('sendPollVote: Non-group conversation; aborting'); - return; - } let sendErrors: Array = []; const saveErrors = (errors: Array): void => { sendErrors = errors; @@ -173,10 +173,14 @@ export async function sendPollVote( if (recipientServiceIdsWithoutMe.length === 0) { jobLog.info('sending sync poll vote message only'); - const groupV2Info = conversation.getGroupV2Info({ - members: recipientServiceIdsWithoutMe, - }); - if (!groupV2Info) { + + const groupV2Info = isDirectConversation(conversation.attributes) + ? undefined + : conversation.getGroupV2Info({ + members: recipientServiceIdsWithoutMe, + }); + + if (!groupV2Info && !isDirectConversation(conversation.attributes)) { jobLog.error( 'sendPollVote: Missing groupV2Info for group conversation' ); @@ -207,52 +211,107 @@ export async function sendPollVote( } else { const sendOptions = await getSendOptions(conversation.attributes); - const promise = conversation.queueJob( - 'conversationQueue/sendPollVote', - async abortSignal => { - const groupV2Info = conversation.getGroupV2Info({ - members: recipientServiceIdsWithoutMe, - }); - strictAssert( - groupV2Info, - 'could not get group info from conversation' + let promise: Promise; + + if (isDirectConversation(conversation.attributes)) { + if (!isConversationAccepted(conversation.attributes)) { + jobLog.info( + `conversation ${conversation.idForLogging()} is not accepted; refusing to send` ); - - if (revision != null) { - groupV2Info.revision = revision; - } - - const contentMessage = await messaging.getPollVoteContentMessage({ - groupV2: groupV2Info, - timestamp: currentTimestamp, - profileKey, - expireTimer, - expireTimerVersion: conversation.getExpireTimerVersion(), - pollVote: { - targetAuthorAci: data.targetAuthorAci, - targetTimestamp: data.targetTimestamp, - optionIndexes: currentOptionIndexes, - voteCount: currentVoteCount, - }, - }); - - if (abortSignal?.aborted) { - throw new Error('sendPollVote was aborted'); - } - - return sendContentMessageToGroup({ - contentHint: ContentHint.Resendable, - contentMessage, - messageId: pollMessageId, - recipients: recipientServiceIdsWithoutMe, - sendOptions, - sendTarget: conversation.toSenderKeyTarget(), - sendType: 'pollVote', - timestamp: currentTimestamp, - urgent: true, - }); + 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` + ); + return; + } + + jobLog.info('sending direct poll vote message'); + const contentMessage = await messaging.getPollVoteContentMessage({ + groupV2: undefined, + timestamp: currentTimestamp, + profileKey, + expireTimer, + expireTimerVersion: conversation.getExpireTimerVersion(), + pollVote: { + targetAuthorAci: data.targetAuthorAci, + targetTimestamp: data.targetTimestamp, + optionIndexes: currentOptionIndexes, + voteCount: currentVoteCount, + }, + }); + + addPniSignatureMessageToProto({ + conversation, + proto: contentMessage, + reason: `sendPollVote(${currentTimestamp})`, + }); + + promise = messaging.sendMessageProtoAndWait({ + timestamp: currentTimestamp, + recipients: [recipientServiceIdsWithoutMe[0]], + proto: contentMessage, + contentHint: ContentHint.Resendable, + groupId: undefined, + options: sendOptions, + urgent: true, + }); + } else { + jobLog.info('sending group poll vote message'); + promise = conversation.queueJob( + 'conversationQueue/sendPollVote', + async abortSignal => { + const groupV2Info = conversation.getGroupV2Info({ + members: recipientServiceIdsWithoutMe, + }); + strictAssert( + groupV2Info, + 'could not get group info from conversation' + ); + + if (revision != null) { + groupV2Info.revision = revision; + } + + const contentMessage = await messaging.getPollVoteContentMessage({ + groupV2: groupV2Info, + timestamp: currentTimestamp, + profileKey, + expireTimer, + expireTimerVersion: conversation.getExpireTimerVersion(), + pollVote: { + targetAuthorAci: data.targetAuthorAci, + targetTimestamp: data.targetTimestamp, + optionIndexes: currentOptionIndexes, + voteCount: currentVoteCount, + }, + }); + + if (abortSignal?.aborted) { + throw new Error('sendPollVote was aborted'); + } + + return sendContentMessageToGroup({ + contentHint: ContentHint.Resendable, + contentMessage, + messageId: pollMessageId, + recipients: recipientServiceIdsWithoutMe, + sendOptions, + sendTarget: conversation.toSenderKeyTarget(), + sendType: 'pollVote', + timestamp: currentTimestamp, + urgent: true, + }); + } + ); + } const messageSendPromise = send(ephemeral, { promise: handleMessageSend(promise, { diff --git a/ts/messages/handleDataMessage.preload.ts b/ts/messages/handleDataMessage.preload.ts index 3cc4e857a8..12e2ff6914 100644 --- a/ts/messages/handleDataMessage.preload.ts +++ b/ts/messages/handleDataMessage.preload.ts @@ -477,13 +477,6 @@ export async function handleDataMessage( confirm(); return; } - if (!isGroup(conversation.attributes)) { - log.warn( - `${idLog}: Dropping PollCreate in non-group conversation ${conversation.idForLogging()}` - ); - confirm(); - return; - } const result = safeParsePartial( PollCreateSchema, initialMessage.pollCreate diff --git a/ts/polls/enqueuePollVoteForSend.preload.ts b/ts/polls/enqueuePollVoteForSend.preload.ts index a6aba65041..e303e64b6e 100644 --- a/ts/polls/enqueuePollVoteForSend.preload.ts +++ b/ts/polls/enqueuePollVoteForSend.preload.ts @@ -15,7 +15,6 @@ import type { PollVoteAttributesType } from '../messageModifiers/Polls.preload.j import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.std.js'; import { getSourceServiceId } from '../messages/sources.preload.js'; import { isAciString } from '../util/isAciString.std.js'; -import { isGroup } from '../util/whatTypeOfConversation.dom.js'; import { strictAssert } from '../util/assert.std.js'; import { createLogger } from '../logging/log.std.js'; @@ -38,10 +37,6 @@ export async function enqueuePollVoteForSend({ conversation, 'enqueuePollVoteForSend: No conversation extracted from target message' ); - strictAssert( - isGroup(conversation.attributes), - 'enqueuePollVoteForSend: conversation must be a group' - ); const timestamp = Date.now(); const targetAuthorAci = getSourceServiceId(message.attributes); diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index 02cfbe8997..6302891196 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -243,9 +243,12 @@ export type GroupMessageOptionsType = Readonly< >; export type PollVoteBuildOptions = Required< - Pick + Pick > & - Pick; + Pick< + MessageOptionsType, + 'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion' + >; export type PollTerminateBuildOptions = Required< Pick @@ -689,13 +692,13 @@ class Message { } } -type AddPniSignatureMessageToProtoOptionsType = Readonly<{ +export type AddPniSignatureMessageToProtoOptionsType = Readonly<{ conversation?: ConversationModel; proto: Proto.Content; reason: string; }>; -function addPniSignatureMessageToProto({ +export function addPniSignatureMessageToProto({ conversation, proto, reason, @@ -838,10 +841,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; diff --git a/ts/types/Polls.dom.ts b/ts/types/Polls.dom.ts index 49f995cf80..514e496831 100644 --- a/ts/types/Polls.dom.ts +++ b/ts/types/Polls.dom.ts @@ -14,7 +14,10 @@ import type { SendStateByConversationId } from '../messages/MessageSendState.std import { aciSchema } from './ServiceId.std.js'; import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js'; -export const POLL_QUESTION_MAX_LENGTH = 100; +// temporarily limit poll questions to an outbound 100 char and an inbound 200 char +export const POLL_QUESTION_MAX_LENGTH_RECEIVE = 200; +export const POLL_QUESTION_MAX_LENGTH_SEND = 100; +export const POLL_OPTION_MAX_LENGTH = 100; export const POLL_OPTIONS_MIN_COUNT = 2; export const POLL_OPTIONS_MAX_COUNT = 10; @@ -27,9 +30,12 @@ export const PollCreateSchema = z question: z .string() .min(1) - .refine(value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH), { - message: `question must contain at most ${POLL_QUESTION_MAX_LENGTH} characters`, - }) + .refine( + value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH_RECEIVE), + { + message: `question must contain at most ${POLL_QUESTION_MAX_LENGTH_RECEIVE} characters`, + } + ) .refine( value => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH, { @@ -41,12 +47,9 @@ export const PollCreateSchema = z z .string() .min(1) - .refine( - value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH), - { - message: `option must contain at most ${POLL_QUESTION_MAX_LENGTH} characters`, - } - ) + .refine(value => hasAtMostGraphemes(value, POLL_OPTION_MAX_LENGTH), { + message: `option must contain at most ${POLL_OPTION_MAX_LENGTH} characters`, + }) .refine( value => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH, {