mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
Support sending pin/unpin messages
Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
@@ -57,6 +57,8 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.std.js'
|
||||
import { FIBONACCI } from '../util/BackOff.std.js';
|
||||
import { parseUnknown } from '../util/schemas.std.js';
|
||||
import { challengeHandler } from '../services/challengeHandler.preload.js';
|
||||
import { sendPinMessage } from './helpers/sendPinMessage.preload.js';
|
||||
import { sendUnpinMessage } from './helpers/sendUnpinMessage.preload.js';
|
||||
|
||||
const globalLogger = createLogger('conversationJobQueue');
|
||||
|
||||
@@ -71,6 +73,7 @@ export const conversationQueueJobEnum = z.enum([
|
||||
'GroupUpdate',
|
||||
'NormalMessage',
|
||||
'NullMessage',
|
||||
'PinMessage',
|
||||
'PollTerminate',
|
||||
'PollVote',
|
||||
'ProfileKey',
|
||||
@@ -81,6 +84,7 @@ export const conversationQueueJobEnum = z.enum([
|
||||
'SenderKeyDistribution',
|
||||
'Story',
|
||||
'Receipts',
|
||||
'UnpinMessage',
|
||||
]);
|
||||
type ConversationQueueJobEnum = z.infer<typeof conversationQueueJobEnum>;
|
||||
|
||||
@@ -198,6 +202,16 @@ const reactionJobDataSchema = z.object({
|
||||
});
|
||||
export type ReactionJobData = z.infer<typeof reactionJobDataSchema>;
|
||||
|
||||
const pinMessageJobDataSchema = z.object({
|
||||
type: z.literal(conversationQueueJobEnum.enum.PinMessage),
|
||||
conversationId: z.string(),
|
||||
targetMessageId: z.string(),
|
||||
targetAuthorAci: aciSchema,
|
||||
targetSentTimestamp: z.number(),
|
||||
pinDurationSeconds: z.number().nullable(),
|
||||
});
|
||||
export type PinMessageJobData = z.infer<typeof pinMessageJobDataSchema>;
|
||||
|
||||
const pollVoteJobDataSchema = z.object({
|
||||
type: z.literal(conversationQueueJobEnum.enum.PollVote),
|
||||
conversationId: z.string(),
|
||||
@@ -270,6 +284,15 @@ const receiptsJobDataSchema = z.object({
|
||||
});
|
||||
export type ReceiptsJobData = z.infer<typeof receiptsJobDataSchema>;
|
||||
|
||||
const unpinMessageJobDataSchema = z.object({
|
||||
type: z.literal(conversationQueueJobEnum.enum.UnpinMessage),
|
||||
conversationId: z.string(),
|
||||
targetMessageId: z.string(),
|
||||
targetAuthorAci: aciSchema,
|
||||
targetSentTimestamp: z.number(),
|
||||
});
|
||||
export type UnpinMessageJobData = z.infer<typeof unpinMessageJobDataSchema>;
|
||||
|
||||
export const conversationQueueJobDataSchema = z.union([
|
||||
callingMessageJobDataSchema,
|
||||
deleteForEveryoneJobDataSchema,
|
||||
@@ -279,6 +302,7 @@ export const conversationQueueJobDataSchema = z.union([
|
||||
groupUpdateJobDataSchema,
|
||||
normalMessageSendJobDataSchema,
|
||||
nullMessageJobDataSchema,
|
||||
pinMessageJobDataSchema,
|
||||
pollTerminateJobDataSchema,
|
||||
pollVoteJobDataSchema,
|
||||
profileKeyJobDataSchema,
|
||||
@@ -288,6 +312,7 @@ export const conversationQueueJobDataSchema = z.union([
|
||||
senderKeyDistributionJobDataSchema,
|
||||
storyJobDataSchema,
|
||||
receiptsJobDataSchema,
|
||||
unpinMessageJobDataSchema,
|
||||
]);
|
||||
export type ConversationQueueJobData = z.infer<
|
||||
typeof conversationQueueJobDataSchema
|
||||
@@ -330,6 +355,9 @@ function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
|
||||
if (type === 'NullMessage') {
|
||||
return false;
|
||||
}
|
||||
if (type === 'PinMessage') {
|
||||
return true;
|
||||
}
|
||||
if (type === 'ProfileKey') {
|
||||
return false;
|
||||
}
|
||||
@@ -362,6 +390,9 @@ function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
|
||||
if (type === 'Story') {
|
||||
return true;
|
||||
}
|
||||
if (type === 'UnpinMessage') {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
@@ -989,6 +1020,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||
case jobSet.Reaction:
|
||||
await sendReaction(conversation, jobBundle, data);
|
||||
break;
|
||||
case jobSet.PinMessage:
|
||||
await sendPinMessage(conversation, jobBundle, data);
|
||||
break;
|
||||
case jobSet.PollTerminate:
|
||||
await sendPollTerminate(conversation, jobBundle, data);
|
||||
break;
|
||||
@@ -1010,6 +1044,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
||||
case jobSet.Receipts:
|
||||
await sendReceipts(conversation, jobBundle, data);
|
||||
break;
|
||||
case jobSet.UnpinMessage:
|
||||
await sendUnpinMessage(conversation, jobBundle, data);
|
||||
break;
|
||||
default: {
|
||||
// Note: This should never happen, because the zod call in parseData wouldn't
|
||||
// accept data that doesn't look like our type specification.
|
||||
|
||||
195
ts/jobs/helpers/createSendMessageJob.preload.ts
Normal file
195
ts/jobs/helpers/createSendMessageJob.preload.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ContentHint } from '@signalapp/libsignal-client';
|
||||
import type { ConversationModel } from '../../models/conversations.preload.js';
|
||||
import { getSendOptions } from '../../util/getSendOptions.preload.js';
|
||||
import { sendToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import {
|
||||
isDirectConversation,
|
||||
isGroupV2,
|
||||
} from '../../util/whatTypeOfConversation.dom.js';
|
||||
import type { ConversationQueueJobBundle } from '../conversationJobQueue.preload.js';
|
||||
import { getSendRecipientLists } from './getSendRecipientLists.dom.js';
|
||||
import type { SendTypesType } from '../../util/handleMessageSend.preload.js';
|
||||
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
import type { SharedMessageOptionsType } from '../../textsecure/SendMessage.preload.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { wrapWithSyncMessageSend } from '../../util/wrapWithSyncMessageSend.preload.js';
|
||||
import {
|
||||
handleMultipleSendErrors,
|
||||
maybeExpandErrors,
|
||||
} from './handleMultipleSendErrors.std.js';
|
||||
|
||||
export type SendMessageJobOptions<Data> = Readonly<{
|
||||
sendName: string; // ex: 'sendExampleMessage'
|
||||
sendType: SendTypesType;
|
||||
getMessageId: (data: Data) => string | null;
|
||||
getMessageOptions: (
|
||||
data: Data,
|
||||
jobTimestamp: number
|
||||
) => Omit<SharedMessageOptionsType, 'recipients'>;
|
||||
}>;
|
||||
|
||||
export function createSendMessageJob<Data>(
|
||||
options: SendMessageJobOptions<Data>
|
||||
) {
|
||||
return async function sendMessage(
|
||||
conversation: ConversationModel,
|
||||
job: ConversationQueueJobBundle,
|
||||
data: Data
|
||||
): Promise<void> {
|
||||
const { sendName, sendType, getMessageId, getMessageOptions } = options;
|
||||
|
||||
const logId = `${sendName}(${conversation.idForLogging()}/${job.timestamp})`;
|
||||
const log = job.log.child(logId);
|
||||
|
||||
if (!job.shouldContinue) {
|
||||
log.info('Ran out of time, cancelling send');
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
allRecipientServiceIds,
|
||||
recipientServiceIdsWithoutMe,
|
||||
untrustedServiceIds,
|
||||
} = getSendRecipientLists({
|
||||
log,
|
||||
conversation,
|
||||
conversationIds: conversation.getRecipients(),
|
||||
});
|
||||
|
||||
if (untrustedServiceIds.length > 0) {
|
||||
window.reduxActions.conversations.conversationStoppedByMissingVerification(
|
||||
{
|
||||
conversationId: conversation.id,
|
||||
untrustedServiceIds,
|
||||
}
|
||||
);
|
||||
throw new Error(
|
||||
`${sendType} blocked because ${untrustedServiceIds.length} ` +
|
||||
'conversation(s) were untrusted. Failing this attempt.'
|
||||
);
|
||||
}
|
||||
|
||||
const messageId = getMessageId(data);
|
||||
const messageOptions = getMessageOptions(data, job.timestamp);
|
||||
|
||||
try {
|
||||
if (recipientServiceIdsWithoutMe.length === 0) {
|
||||
const sendOptions = await getSendOptions(conversation.attributes, {
|
||||
syncMessage: true,
|
||||
});
|
||||
// Only sending a sync to ourselves
|
||||
await conversation.queueJob(
|
||||
`conversationQueue/${sendName}/sync`,
|
||||
async () => {
|
||||
const encodedDataMessage = await job.messaging.getDataOrEditMessage(
|
||||
{
|
||||
...messageOptions,
|
||||
recipients: allRecipientServiceIds,
|
||||
}
|
||||
);
|
||||
|
||||
return handleMessageSend(
|
||||
job.messaging.sendSyncMessage({
|
||||
encodedDataMessage,
|
||||
timestamp: job.timestamp,
|
||||
destinationE164: conversation.get('e164'),
|
||||
destinationServiceId: conversation.getServiceId(),
|
||||
expirationStartTimestamp: null,
|
||||
isUpdate: false,
|
||||
options: sendOptions,
|
||||
urgent: false,
|
||||
}),
|
||||
{
|
||||
messageIds: messageId != null ? [messageId] : [],
|
||||
sendType,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} else if (isDirectConversation(conversation.attributes)) {
|
||||
const recipientServiceId = recipientServiceIdsWithoutMe.at(0);
|
||||
|
||||
if (recipientServiceId == null) {
|
||||
log.info('Recipient was dropped');
|
||||
return;
|
||||
}
|
||||
|
||||
const sendOptions = await getSendOptions(conversation.attributes);
|
||||
|
||||
await conversation.queueJob(
|
||||
`conversationQueue/${sendName}/direct`,
|
||||
() => {
|
||||
return wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId,
|
||||
messageIds: messageId != null ? [messageId] : [],
|
||||
send: sender => {
|
||||
return sender.sendMessageToServiceId({
|
||||
serviceId: recipientServiceId,
|
||||
messageOptions: getMessageOptions(data, job.timestamp),
|
||||
groupId: undefined,
|
||||
contentHint: ContentHint.Resendable,
|
||||
options: sendOptions,
|
||||
urgent: true,
|
||||
includePniSignatureMessage: true,
|
||||
});
|
||||
},
|
||||
sendType,
|
||||
timestamp: job.timestamp,
|
||||
});
|
||||
}
|
||||
);
|
||||
} else if (isGroupV2(conversation.attributes)) {
|
||||
const sendOptions = await getSendOptions(conversation.attributes, {
|
||||
groupId: conversation.get('groupId'),
|
||||
});
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
strictAssert(groupV2Info, 'Missing groupV2Info');
|
||||
|
||||
await conversation.queueJob(
|
||||
`conversationQueue/${sendName}/group`,
|
||||
abortSignal => {
|
||||
return wrapWithSyncMessageSend({
|
||||
conversation,
|
||||
logId,
|
||||
messageIds: messageId != null ? [messageId] : [],
|
||||
send: () => {
|
||||
return sendToGroup({
|
||||
abortSignal,
|
||||
contentHint: ContentHint.Resendable,
|
||||
groupSendOptions: {
|
||||
groupV2: groupV2Info,
|
||||
...getMessageOptions(data, job.timestamp),
|
||||
},
|
||||
messageId: messageId ?? undefined,
|
||||
sendOptions,
|
||||
sendTarget: conversation.toSenderKeyTarget(),
|
||||
sendType,
|
||||
urgent: true,
|
||||
});
|
||||
},
|
||||
sendType,
|
||||
timestamp: job.timestamp,
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw new Error('Unexpected conversation type');
|
||||
}
|
||||
} catch (error) {
|
||||
const errors = maybeExpandErrors(error);
|
||||
await handleMultipleSendErrors({
|
||||
errors,
|
||||
isFinalAttempt: job.isFinalAttempt,
|
||||
log,
|
||||
timeRemaining: job.timeRemaining,
|
||||
toThrow: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
98
ts/jobs/helpers/getSendRecipientLists.dom.ts
Normal file
98
ts/jobs/helpers/getSendRecipientLists.dom.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ServiceIdString } from '@signalapp/mock-server/src/types';
|
||||
import type { ConversationModel } from '../../models/conversations.preload.js';
|
||||
import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import { isMe } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation.dom.js';
|
||||
|
||||
export type SendRecipientLists = Readonly<{
|
||||
allRecipientServiceIds: Array<ServiceIdString>;
|
||||
recipientServiceIdsWithoutMe: Array<ServiceIdString>;
|
||||
untrustedServiceIds: Array<ServiceIdString>;
|
||||
}>;
|
||||
|
||||
export type GetSendRecipientListsOptions = Readonly<{
|
||||
log: LoggerType;
|
||||
conversationIds: ReadonlyArray<string>;
|
||||
conversation: ConversationModel;
|
||||
}>;
|
||||
|
||||
export function getSendRecipientLists(
|
||||
options: GetSendRecipientListsOptions
|
||||
): SendRecipientLists {
|
||||
const { log, conversationIds, conversation } = options;
|
||||
|
||||
const allRecipientServiceIds: Array<ServiceIdString> = [];
|
||||
const recipientServiceIdsWithoutMe: Array<ServiceIdString> = [];
|
||||
const untrustedServiceIds: Array<ServiceIdString> = [];
|
||||
|
||||
const memberConversationIds = conversation.getMemberConversationIds();
|
||||
|
||||
for (const conversationId of conversationIds) {
|
||||
const recipient = window.ConversationController.get(conversationId);
|
||||
if (!recipient) {
|
||||
log.warn(
|
||||
`getRecipients/${conversationId}: Missing conversation, dropping recipient`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const logPrefix = `getRecipients/${recipient.idForLogging()}`;
|
||||
|
||||
const sendTarget = recipient.getSendTarget();
|
||||
if (sendTarget == null) {
|
||||
log.warn(`${logPrefix}: Missing send target, dropping recipient`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isRecipientMe = isMe(recipient.attributes);
|
||||
const isRecipientMember = memberConversationIds.has(conversationId);
|
||||
|
||||
if (!(isRecipientMember || isRecipientMe)) {
|
||||
log.warn(
|
||||
`${logPrefix}: Recipient is not a member of conversation, dropping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isUntrusted()) {
|
||||
const serviceId = recipient.getServiceId();
|
||||
if (!serviceId) {
|
||||
log.error(
|
||||
`${logPrefix}: Recipient is untrusted and missing serviceId, dropping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
untrustedServiceIds.push(serviceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isUnregistered()) {
|
||||
log.warn(`${logPrefix}: Recipient is unregistered, dropping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isBlocked()) {
|
||||
log.warn(`${logPrefix}: Recipient is blocked, dropping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSignalConversation(recipient.attributes)) {
|
||||
log.info(`${logPrefix}: Recipient is Signal conversation, dropping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
allRecipientServiceIds.push(sendTarget);
|
||||
if (!isRecipientMe) {
|
||||
recipientServiceIdsWithoutMe.push(sendTarget);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allRecipientServiceIds,
|
||||
recipientServiceIdsWithoutMe,
|
||||
untrustedServiceIds,
|
||||
};
|
||||
}
|
||||
@@ -199,15 +199,13 @@ export async function sendDeleteForEveryone(
|
||||
sender.sendMessageToServiceId({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
serviceId: conversation.getSendTarget()!,
|
||||
messageText: undefined,
|
||||
attachments: [],
|
||||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
expireTimer: undefined,
|
||||
expireTimerVersion: undefined,
|
||||
messageOptions: {
|
||||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
profileKey,
|
||||
},
|
||||
contentHint,
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
options: sendOptions,
|
||||
urgent: true,
|
||||
story,
|
||||
@@ -226,7 +224,8 @@ export async function sendDeleteForEveryone(
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipients,
|
||||
});
|
||||
if (groupV2Info && isNumber(revision)) {
|
||||
strictAssert(groupV2Info, 'Missing groupV2Info');
|
||||
if (isNumber(revision)) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
|
||||
@@ -180,17 +180,15 @@ export async function sendDeleteStoryForEveryone(
|
||||
await handleMessageSend(
|
||||
messaging.sendMessageToServiceId({
|
||||
serviceId,
|
||||
messageText: undefined,
|
||||
attachments: [],
|
||||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
expireTimer: undefined,
|
||||
expireTimerVersion: undefined,
|
||||
contentHint,
|
||||
messageOptions: {
|
||||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
profileKey: conversation.get('profileSharing')
|
||||
? profileKey
|
||||
: undefined,
|
||||
},
|
||||
groupId: undefined,
|
||||
profileKey: conversation.get('profileSharing')
|
||||
? profileKey
|
||||
: undefined,
|
||||
contentHint,
|
||||
options: sendOptions,
|
||||
urgent: true,
|
||||
story: true,
|
||||
|
||||
@@ -340,7 +340,8 @@ export async function sendNormalMessage(
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
if (groupV2Info && isNumber(revision)) {
|
||||
strictAssert(groupV2Info, 'Missing groupV2Info');
|
||||
if (isNumber(revision)) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
@@ -358,7 +359,7 @@ export async function sendNormalMessage(
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
groupV2: groupV2Info,
|
||||
messageText: body,
|
||||
body,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
@@ -416,27 +417,29 @@ export async function sendNormalMessage(
|
||||
|
||||
log.info('sending direct message');
|
||||
innerPromise = messaging.sendMessageToServiceId({
|
||||
attachments,
|
||||
bodyRanges,
|
||||
contact,
|
||||
contentHint: ContentHint.Resendable,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
groupId: undefined,
|
||||
serviceId: recipientServiceIdsWithoutMe[0],
|
||||
messageText: body,
|
||||
messageOptions: {
|
||||
attachments,
|
||||
body,
|
||||
bodyRanges,
|
||||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
sticker,
|
||||
storyContext,
|
||||
reaction,
|
||||
targetTimestampForEdit: editedMessageTimestamp
|
||||
? targetOfThisEditTimestamp
|
||||
: undefined,
|
||||
timestamp: targetTimestamp,
|
||||
},
|
||||
contentHint: ContentHint.Resendable,
|
||||
groupId: undefined,
|
||||
options: sendOptions,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
sticker,
|
||||
storyContext,
|
||||
reaction,
|
||||
targetTimestampForEdit: editedMessageTimestamp
|
||||
? targetOfThisEditTimestamp
|
||||
: undefined,
|
||||
timestamp: targetTimestamp,
|
||||
// Note: 1:1 story replies should not set story=true - they aren't group sends
|
||||
urgent: true,
|
||||
includePniSignatureMessage: true,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { MessageSender } from '../../textsecure/SendMessage.preload.js';
|
||||
import { sendToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
|
||||
async function clearResetsTracking(idForTracking: string | undefined) {
|
||||
if (!idForTracking) {
|
||||
@@ -101,9 +102,8 @@ export async function sendNullMessage(
|
||||
);
|
||||
} else {
|
||||
const groupV2Info = conversation.getGroupV2Info();
|
||||
if (groupV2Info) {
|
||||
groupV2Info.revision = 0;
|
||||
}
|
||||
strictAssert(groupV2Info, 'Missing groupV2Info');
|
||||
groupV2Info.revision = 0;
|
||||
|
||||
await conversation.queueJob(
|
||||
'conversationQueue/sendNullMessage/group',
|
||||
@@ -118,7 +118,7 @@ export async function sendNullMessage(
|
||||
deletedForEveryoneTimestamp: undefined,
|
||||
expireTimer: undefined,
|
||||
groupV2: groupV2Info,
|
||||
messageText: undefined,
|
||||
body: undefined,
|
||||
preview: [],
|
||||
profileKey: undefined,
|
||||
quote: undefined,
|
||||
|
||||
22
ts/jobs/helpers/sendPinMessage.preload.ts
Normal file
22
ts/jobs/helpers/sendPinMessage.preload.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { PinMessageJobData } from '../conversationJobQueue.preload.js';
|
||||
import { createSendMessageJob } from './createSendMessageJob.preload.js';
|
||||
|
||||
export const sendPinMessage = createSendMessageJob<PinMessageJobData>({
|
||||
sendName: 'sendPinMessage',
|
||||
sendType: 'pinMessage',
|
||||
getMessageId(data) {
|
||||
return data.targetMessageId;
|
||||
},
|
||||
getMessageOptions(data, jobTimestamp) {
|
||||
return {
|
||||
timestamp: jobTimestamp,
|
||||
pinMessage: {
|
||||
targetAuthorAci: data.targetAuthorAci,
|
||||
targetSentTimestamp: data.targetSentTimestamp,
|
||||
pinDurationSeconds: data.pinDurationSeconds,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { ContentHint } from '@signalapp/libsignal-client';
|
||||
import * as Errors from '../../types/errors.std.js';
|
||||
import { isGroupV2, isMe } from '../../util/whatTypeOfConversation.dom.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';
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
SendStatus,
|
||||
type SendStateByConversationId,
|
||||
} from '../../messages/MessageSendState.std.js';
|
||||
import type { ServiceIdString } from '../../types/ServiceId.std.js';
|
||||
import type { LoggerType } from '../../types/Logging.std.js';
|
||||
import type { MessagePollVoteType } from '../../types/Polls.dom.js';
|
||||
import type { ConversationModel } from '../../models/conversations.preload.js';
|
||||
import type {
|
||||
@@ -29,6 +27,7 @@ import type {
|
||||
} from '../conversationJobQueue.preload.js';
|
||||
import * as pollVoteUtil from '../../polls/util.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { getSendRecipientLists } from './getSendRecipientLists.dom.js';
|
||||
|
||||
export async function sendPollVote(
|
||||
conversation: ConversationModel,
|
||||
@@ -122,11 +121,15 @@ export async function sendPollVote(
|
||||
const currentOptionIndexes = [...currentPendingVote.optionIndexes];
|
||||
const currentTimestamp = currentPendingVote.timestamp;
|
||||
|
||||
const { recipientServiceIdsWithoutMe, untrustedServiceIds } = getRecipients(
|
||||
jobLog,
|
||||
currentPendingVote,
|
||||
conversation
|
||||
const unsentConversationIds = Array.from(
|
||||
pollVoteUtil.getUnsentConversationIds(currentPendingVote)
|
||||
);
|
||||
const { recipientServiceIdsWithoutMe, untrustedServiceIds } =
|
||||
getSendRecipientLists({
|
||||
log: jobLog,
|
||||
conversationIds: unsentConversationIds,
|
||||
conversation,
|
||||
});
|
||||
|
||||
if (untrustedServiceIds.length) {
|
||||
window.reduxActions.conversations.conversationStoppedByMissingVerification(
|
||||
@@ -145,9 +148,6 @@ export async function sendPollVote(
|
||||
? await ourProfileKeyService.get()
|
||||
: undefined;
|
||||
|
||||
const unsentConversationIds = Array.from(
|
||||
pollVoteUtil.getUnsentConversationIds(currentPendingVote)
|
||||
);
|
||||
const ephemeral = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
type: 'outgoing',
|
||||
@@ -213,15 +213,15 @@ export async function sendPollVote(
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
if (groupV2Info && revision != null) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
strictAssert(
|
||||
groupV2Info,
|
||||
'could not get group info from conversation'
|
||||
);
|
||||
|
||||
if (revision != null) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
const contentMessage = await messaging.getPollVoteContentMessage({
|
||||
groupV2: groupV2Info,
|
||||
timestamp: currentTimestamp,
|
||||
@@ -338,70 +338,3 @@ export async function sendPollVote(
|
||||
await window.MessageCache.saveMessage(pollMessage.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
function getRecipients(
|
||||
log: LoggerType,
|
||||
pendingVote: MessagePollVoteType,
|
||||
conversation: ConversationModel
|
||||
): {
|
||||
allRecipientServiceIds: Array<ServiceIdString>;
|
||||
recipientServiceIdsWithoutMe: Array<ServiceIdString>;
|
||||
untrustedServiceIds: Array<ServiceIdString>;
|
||||
} {
|
||||
const allRecipientServiceIds: Array<ServiceIdString> = [];
|
||||
const recipientServiceIdsWithoutMe: Array<ServiceIdString> = [];
|
||||
const untrustedServiceIds: Array<ServiceIdString> = [];
|
||||
|
||||
const currentConversationRecipients = conversation.getMemberConversationIds();
|
||||
|
||||
// Only send to recipients who haven't received this vote yet
|
||||
for (const conversationId of pollVoteUtil.getUnsentConversationIds(
|
||||
pendingVote
|
||||
)) {
|
||||
const recipient = window.ConversationController.get(conversationId);
|
||||
if (!recipient) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recipientIdentifier = recipient.getSendTarget();
|
||||
const isRecipientMe = isMe(recipient.attributes);
|
||||
|
||||
if (
|
||||
!recipientIdentifier ||
|
||||
(!currentConversationRecipients.has(conversationId) && !isRecipientMe)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isUntrusted()) {
|
||||
const serviceId = recipient.getServiceId();
|
||||
if (!serviceId) {
|
||||
log.error(
|
||||
`sendPollVote/getRecipients: Recipient ${recipient.idForLogging()} is untrusted but has no serviceId`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
untrustedServiceIds.push(serviceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isUnregistered()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isBlocked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
allRecipientServiceIds.push(recipientIdentifier);
|
||||
if (!isRecipientMe) {
|
||||
recipientServiceIdsWithoutMe.push(recipientIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allRecipientServiceIds,
|
||||
recipientServiceIdsWithoutMe,
|
||||
untrustedServiceIds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { shouldSendToConversation } from './shouldSendToConversation.preload.js';
|
||||
import { sendToGroup } from '../../util/sendToGroup.preload.js';
|
||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
|
||||
const { isNumber } = lodash;
|
||||
|
||||
@@ -148,7 +149,8 @@ export async function sendProfileKey(
|
||||
}
|
||||
|
||||
const groupV2Info = conversation.getGroupV2Info();
|
||||
if (groupV2Info && isNumber(revision)) {
|
||||
strictAssert(groupV2Info, 'Missing groupV2Info');
|
||||
if (isNumber(revision)) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { isSent, SendStatus } from '../../messages/MessageSendState.std.js';
|
||||
import { getMessageById } from '../../messages/getMessageById.preload.js';
|
||||
import { isIncoming } from '../../messages/helpers.std.js';
|
||||
import {
|
||||
isMe,
|
||||
isDirectConversation,
|
||||
isGroupV2,
|
||||
} from '../../util/whatTypeOfConversation.dom.js';
|
||||
@@ -26,7 +25,7 @@ import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
import { ourProfileKeyService } from '../../services/ourProfileKey.std.js';
|
||||
import { canReact, isStory } from '../../state/selectors/message.preload.js';
|
||||
import { findAndFormatContact } from '../../util/findAndFormatContact.preload.js';
|
||||
import type { AciString, ServiceIdString } from '../../types/ServiceId.std.js';
|
||||
import type { AciString } from '../../types/ServiceId.std.js';
|
||||
import { isAciString } from '../../util/isAciString.std.js';
|
||||
import { handleMultipleSendErrors } from './handleMultipleSendErrors.std.js';
|
||||
import { incrementMessageCounter } from '../../util/incrementMessageCounter.preload.js';
|
||||
@@ -38,11 +37,11 @@ import type {
|
||||
} from '../conversationJobQueue.preload.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import type { LoggerType } from '../../types/Logging.std.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';
|
||||
|
||||
const { isNumber } = lodash;
|
||||
|
||||
@@ -123,11 +122,18 @@ export async function sendReaction(
|
||||
}
|
||||
|
||||
const expireTimer = messageConversation.get('expireTimer');
|
||||
const unsentConversationIds = Array.from(
|
||||
reactionUtil.getUnsentConversationIds(pendingReaction)
|
||||
);
|
||||
const {
|
||||
allRecipientServiceIds,
|
||||
recipientServiceIdsWithoutMe,
|
||||
untrustedServiceIds,
|
||||
} = getRecipients(log, pendingReaction, conversation);
|
||||
} = getSendRecipientLists({
|
||||
log,
|
||||
conversationIds: unsentConversationIds,
|
||||
conversation,
|
||||
});
|
||||
|
||||
if (untrustedServiceIds.length) {
|
||||
window.reduxActions.conversations.conversationStoppedByMissingVerification(
|
||||
@@ -242,19 +248,15 @@ export async function sendReaction(
|
||||
log.info('sending direct reaction message');
|
||||
promise = messaging.sendMessageToServiceId({
|
||||
serviceId: recipientServiceIdsWithoutMe[0],
|
||||
messageText: undefined,
|
||||
attachments: [],
|
||||
quote: undefined,
|
||||
preview: [],
|
||||
sticker: undefined,
|
||||
reaction: reactionForSend,
|
||||
deletedForEveryoneTimestamp: undefined,
|
||||
timestamp: pendingReaction.timestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
contentHint: ContentHint.Resendable,
|
||||
messageOptions: {
|
||||
reaction: reactionForSend,
|
||||
timestamp: pendingReaction.timestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
profileKey,
|
||||
},
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
contentHint: ContentHint.Resendable,
|
||||
options: sendOptions,
|
||||
urgent: true,
|
||||
includePniSignatureMessage: true,
|
||||
@@ -272,7 +274,8 @@ export async function sendReaction(
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
if (groupV2Info && isNumber(revision)) {
|
||||
strictAssert(groupV2Info, 'Missing groupV2Info');
|
||||
if (isNumber(revision)) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
@@ -393,68 +396,6 @@ const setReactions = (
|
||||
}
|
||||
};
|
||||
|
||||
function getRecipients(
|
||||
log: LoggerType,
|
||||
reaction: Readonly<MessageReactionType>,
|
||||
conversation: ConversationModel
|
||||
): {
|
||||
allRecipientServiceIds: Array<ServiceIdString>;
|
||||
recipientServiceIdsWithoutMe: Array<ServiceIdString>;
|
||||
untrustedServiceIds: Array<ServiceIdString>;
|
||||
} {
|
||||
const allRecipientServiceIds: Array<ServiceIdString> = [];
|
||||
const recipientServiceIdsWithoutMe: Array<ServiceIdString> = [];
|
||||
const untrustedServiceIds: Array<ServiceIdString> = [];
|
||||
|
||||
const currentConversationRecipients = conversation.getMemberConversationIds();
|
||||
|
||||
for (const id of reactionUtil.getUnsentConversationIds(reaction)) {
|
||||
const recipient = window.ConversationController.get(id);
|
||||
if (!recipient) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recipientIdentifier = recipient.getSendTarget();
|
||||
const isRecipientMe = isMe(recipient.attributes);
|
||||
|
||||
if (
|
||||
!recipientIdentifier ||
|
||||
(!currentConversationRecipients.has(id) && !isRecipientMe)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.isUntrusted()) {
|
||||
const serviceId = recipient.getServiceId();
|
||||
if (!serviceId) {
|
||||
log.error(
|
||||
`getRecipients: Untrusted conversation ${recipient.idForLogging()} missing serviceId.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
untrustedServiceIds.push(serviceId);
|
||||
continue;
|
||||
}
|
||||
if (recipient.isUnregistered()) {
|
||||
continue;
|
||||
}
|
||||
if (recipient.isBlocked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
allRecipientServiceIds.push(recipientIdentifier);
|
||||
if (!isRecipientMe) {
|
||||
recipientServiceIdsWithoutMe.push(recipientIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allRecipientServiceIds,
|
||||
recipientServiceIdsWithoutMe,
|
||||
untrustedServiceIds,
|
||||
};
|
||||
}
|
||||
|
||||
function markReactionFailed(
|
||||
message: MessageModel,
|
||||
pendingReaction: MessageReactionType
|
||||
|
||||
22
ts/jobs/helpers/sendUnpinMessage.preload.ts
Normal file
22
ts/jobs/helpers/sendUnpinMessage.preload.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { UnpinMessageJobData } from '../conversationJobQueue.preload.js';
|
||||
import { createSendMessageJob } from './createSendMessageJob.preload.js';
|
||||
|
||||
export const sendUnpinMessage = createSendMessageJob<UnpinMessageJobData>({
|
||||
sendName: 'sendUnpinMessage',
|
||||
sendType: 'unpinMessage',
|
||||
getMessageId(data) {
|
||||
return data.targetMessageId;
|
||||
},
|
||||
getMessageOptions(data, jobTimestamp) {
|
||||
return {
|
||||
timestamp: jobTimestamp,
|
||||
unpinMessage: {
|
||||
targetAuthorAci: data.targetAuthorAci,
|
||||
targetSentTimestamp: data.targetSentTimestamp,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -104,6 +104,10 @@ import type { GroupSendToken } from '../types/GroupSendEndorsements.std.js';
|
||||
import type { OutgoingPollVote, PollCreateType } from '../types/Polls.dom.js';
|
||||
import { itemStorage } from './Storage.preload.js';
|
||||
import { accountManager } from './AccountManager.preload.js';
|
||||
import type {
|
||||
SendPinMessageType,
|
||||
SendUnpinMessageType,
|
||||
} from '../types/PinnedMessage.std.js';
|
||||
|
||||
const log = createLogger('SendMessage');
|
||||
|
||||
@@ -195,61 +199,48 @@ export const singleProtoJobDataSchema = z.object({
|
||||
|
||||
export type SingleProtoJobData = z.infer<typeof singleProtoJobDataSchema>;
|
||||
|
||||
export type MessageOptionsType = {
|
||||
export type SharedMessageOptionsType = Readonly<{
|
||||
// required
|
||||
timestamp: number;
|
||||
// optional
|
||||
attachments?: ReadonlyArray<Proto.IAttachmentPointer>;
|
||||
body?: string;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
expireTimer?: DurationInSeconds;
|
||||
expireTimerVersion: number | undefined;
|
||||
flags?: number;
|
||||
group?: {
|
||||
id: string;
|
||||
type: number;
|
||||
};
|
||||
groupCallUpdate?: GroupCallUpdateType;
|
||||
groupV2?: GroupV2InfoType;
|
||||
needsSync?: boolean;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: OutgoingQuoteType;
|
||||
recipients: ReadonlyArray<ServiceIdString>;
|
||||
sticker?: OutgoingStickerType;
|
||||
reaction?: ReactionType;
|
||||
pinMessage?: SendPinMessageType;
|
||||
pollVote?: OutgoingPollVote;
|
||||
pollCreate?: PollCreateType;
|
||||
pollTerminate?: Readonly<{
|
||||
targetTimestamp: number;
|
||||
}>;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
targetTimestampForEdit?: number;
|
||||
timestamp: number;
|
||||
groupCallUpdate?: GroupCallUpdateType;
|
||||
storyContext?: StoryContextType;
|
||||
};
|
||||
export type GroupSendOptionsType = {
|
||||
attachments?: ReadonlyArray<Proto.IAttachmentPointer>;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
targetTimestampForEdit?: number;
|
||||
expireTimer?: DurationInSeconds;
|
||||
flags?: number;
|
||||
groupCallUpdate?: GroupCallUpdateType;
|
||||
groupV2?: GroupV2InfoType;
|
||||
messageText?: string;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: OutgoingQuoteType;
|
||||
reaction?: ReactionType;
|
||||
sticker?: OutgoingStickerType;
|
||||
storyContext?: StoryContextType;
|
||||
timestamp: number;
|
||||
pollVote?: OutgoingPollVote;
|
||||
pollCreate?: PollCreateType;
|
||||
pollTerminate?: Readonly<{
|
||||
targetTimestamp: number;
|
||||
}>;
|
||||
};
|
||||
targetTimestampForEdit?: number;
|
||||
unpinMessage?: SendUnpinMessageType;
|
||||
}>;
|
||||
|
||||
export type MessageOptionsType = Readonly<
|
||||
SharedMessageOptionsType & {
|
||||
// Not needed for group messages, lives in group state
|
||||
expireTimerVersion?: number | undefined;
|
||||
recipients: ReadonlyArray<ServiceIdString>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GroupMessageOptionsType = Readonly<
|
||||
SharedMessageOptionsType & {
|
||||
groupV2: GroupV2InfoType;
|
||||
}
|
||||
>;
|
||||
|
||||
export type PollVoteBuildOptions = Required<
|
||||
Pick<MessageOptionsType, 'groupV2' | 'timestamp' | 'pollVote'>
|
||||
@@ -276,15 +267,8 @@ class Message {
|
||||
|
||||
flags?: number;
|
||||
|
||||
group?: {
|
||||
id: string;
|
||||
type: number;
|
||||
};
|
||||
|
||||
groupV2?: GroupV2InfoType;
|
||||
|
||||
needsSync?: boolean;
|
||||
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
|
||||
|
||||
profileKey?: Uint8Array;
|
||||
@@ -303,6 +287,9 @@ class Message {
|
||||
targetTimestamp: number;
|
||||
}>;
|
||||
|
||||
pinMessage?: SendPinMessageType;
|
||||
unpinMessage?: SendUnpinMessageType;
|
||||
|
||||
timestamp: number;
|
||||
|
||||
dataMessage?: Proto.DataMessage;
|
||||
@@ -323,9 +310,7 @@ class Message {
|
||||
this.expireTimer = options.expireTimer;
|
||||
this.expireTimerVersion = options.expireTimerVersion;
|
||||
this.flags = options.flags;
|
||||
this.group = options.group;
|
||||
this.groupV2 = options.groupV2;
|
||||
this.needsSync = options.needsSync;
|
||||
this.preview = options.preview;
|
||||
this.profileKey = options.profileKey;
|
||||
this.quote = options.quote;
|
||||
@@ -340,12 +325,14 @@ class Message {
|
||||
this.storyContext = options.storyContext;
|
||||
// Polls
|
||||
this.pollVote = options.pollVote;
|
||||
this.pinMessage = options.pinMessage;
|
||||
this.unpinMessage = options.unpinMessage;
|
||||
|
||||
if (!(this.recipients instanceof Array)) {
|
||||
throw new Error('Invalid recipient list');
|
||||
}
|
||||
|
||||
if (!this.group && !this.groupV2 && this.recipients.length !== 1) {
|
||||
if (!this.groupV2 && this.recipients.length !== 1) {
|
||||
throw new Error('Invalid recipient list for non-group');
|
||||
}
|
||||
|
||||
@@ -370,28 +357,14 @@ class Message {
|
||||
}
|
||||
}
|
||||
if (this.isEndSession()) {
|
||||
if (
|
||||
this.body != null ||
|
||||
this.group != null ||
|
||||
this.attachments.length !== 0
|
||||
) {
|
||||
if (this.body != null || this.attachments.length !== 0) {
|
||||
throw new Error('Invalid end session message');
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof this.timestamp !== 'number' ||
|
||||
(this.body && typeof this.body !== 'string')
|
||||
) {
|
||||
throw new Error('Invalid message body');
|
||||
}
|
||||
if (this.group) {
|
||||
if (
|
||||
typeof this.group.id !== 'string' ||
|
||||
typeof this.group.type !== 'number'
|
||||
) {
|
||||
throw new Error('Invalid group context');
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
typeof this.timestamp !== 'number' ||
|
||||
(this.body && typeof this.body !== 'string')
|
||||
) {
|
||||
throw new Error('Invalid message body');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,6 +655,35 @@ class Message {
|
||||
proto.requiredProtocolVersion = Proto.DataMessage.ProtocolVersion.POLLS;
|
||||
}
|
||||
|
||||
if (this.pinMessage != null) {
|
||||
const { targetAuthorAci, targetSentTimestamp, pinDurationSeconds } =
|
||||
this.pinMessage;
|
||||
|
||||
const pinMessage = new Proto.DataMessage.PinMessage({
|
||||
targetAuthorAciBinary: toAciObject(targetAuthorAci).getRawUuidBytes(),
|
||||
targetSentTimestamp: Long.fromNumber(targetSentTimestamp),
|
||||
});
|
||||
|
||||
if (pinDurationSeconds != null) {
|
||||
pinMessage.pinDurationSeconds = pinDurationSeconds;
|
||||
} else {
|
||||
pinMessage.pinDurationForever = true;
|
||||
}
|
||||
|
||||
proto.pinMessage = pinMessage;
|
||||
}
|
||||
|
||||
if (this.unpinMessage != null) {
|
||||
const { targetAuthorAci, targetSentTimestamp } = this.unpinMessage;
|
||||
|
||||
const unpinMessage = new Proto.DataMessage.UnpinMessage({
|
||||
targetAuthorAciBinary: toAciObject(targetAuthorAci).getRawUuidBytes(),
|
||||
targetSentTimestamp: Long.fromNumber(targetSentTimestamp),
|
||||
});
|
||||
|
||||
proto.unpinMessage = unpinMessage;
|
||||
}
|
||||
|
||||
this.dataMessage = proto;
|
||||
return proto;
|
||||
}
|
||||
@@ -1107,7 +1109,7 @@ export class MessageSender {
|
||||
}
|
||||
|
||||
getAttrsFromGroupOptions(
|
||||
options: Readonly<GroupSendOptionsType>
|
||||
options: Readonly<GroupMessageOptionsType>
|
||||
): MessageOptionsType {
|
||||
const {
|
||||
attachments,
|
||||
@@ -1118,7 +1120,7 @@ export class MessageSender {
|
||||
flags,
|
||||
groupCallUpdate,
|
||||
groupV2,
|
||||
messageText,
|
||||
body,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
@@ -1156,7 +1158,7 @@ export class MessageSender {
|
||||
return {
|
||||
attachments,
|
||||
bodyRanges,
|
||||
body: messageText,
|
||||
body,
|
||||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
@@ -1389,70 +1391,28 @@ export class MessageSender {
|
||||
// You might wonder why this takes a groupId. models/messages.resend() can send a group
|
||||
// message to just one person.
|
||||
async sendMessageToServiceId({
|
||||
attachments,
|
||||
bodyRanges,
|
||||
contact,
|
||||
messageOptions,
|
||||
contentHint,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion,
|
||||
groupId,
|
||||
serviceId,
|
||||
messageText,
|
||||
options,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
reaction,
|
||||
sticker,
|
||||
storyContext,
|
||||
story,
|
||||
targetTimestampForEdit,
|
||||
timestamp,
|
||||
urgent,
|
||||
includePniSignatureMessage,
|
||||
}: Readonly<{
|
||||
attachments: ReadonlyArray<Proto.IAttachmentPointer> | undefined;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
contentHint: number;
|
||||
deletedForEveryoneTimestamp: number | undefined;
|
||||
expireTimer: DurationInSeconds | undefined;
|
||||
expireTimerVersion: number | undefined;
|
||||
groupId: string | undefined;
|
||||
serviceId: ServiceIdString;
|
||||
messageText: string | undefined;
|
||||
groupId: string | undefined;
|
||||
messageOptions: Omit<MessageOptionsType, 'recipients'>;
|
||||
contentHint: number;
|
||||
options?: SendOptionsType;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType> | undefined;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: OutgoingQuoteType;
|
||||
reaction?: ReactionType;
|
||||
sticker?: OutgoingStickerType;
|
||||
storyContext?: StoryContextType;
|
||||
story?: boolean;
|
||||
targetTimestampForEdit?: number;
|
||||
timestamp: number;
|
||||
urgent: boolean;
|
||||
includePniSignatureMessage?: boolean;
|
||||
}>): Promise<CallbackResultType> {
|
||||
return this.sendMessage({
|
||||
messageOptions: {
|
||||
attachments,
|
||||
bodyRanges,
|
||||
body: messageText,
|
||||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
reaction,
|
||||
...messageOptions,
|
||||
recipients: [serviceId],
|
||||
sticker,
|
||||
storyContext,
|
||||
targetTimestampForEdit,
|
||||
timestamp,
|
||||
},
|
||||
contentHint,
|
||||
groupId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { AciString } from '@signalapp/mock-server/src/types.js';
|
||||
import type { MessageAttributesType } from '../model-types.js';
|
||||
import type { ConversationType } from '../state/ducks/conversations.preload.js';
|
||||
|
||||
@@ -21,3 +22,14 @@ export type PinnedMessageRenderData = Readonly<{
|
||||
sender: ConversationType;
|
||||
message: MessageAttributesType;
|
||||
}>;
|
||||
|
||||
export type SendPinMessageType = Readonly<{
|
||||
targetAuthorAci: AciString;
|
||||
targetSentTimestamp: number;
|
||||
pinDurationSeconds: number | null;
|
||||
}>;
|
||||
|
||||
export type SendUnpinMessageType = Readonly<{
|
||||
targetAuthorAci: AciString;
|
||||
targetSentTimestamp: number;
|
||||
}>;
|
||||
|
||||
@@ -31,8 +31,10 @@ export const sendTypesEnum = z.enum([
|
||||
'expirationTimerUpdate', // non-urgent
|
||||
'groupChange', // non-urgent
|
||||
'reaction',
|
||||
'pinMessage',
|
||||
'pollTerminate',
|
||||
'pollVote', // non-urgent
|
||||
'unpinMessage',
|
||||
'typing', // excluded from send log; non-urgent
|
||||
|
||||
// Responding to incoming messages, all non-urgent
|
||||
|
||||
@@ -37,7 +37,7 @@ import { isRecord } from './isRecord.std.js';
|
||||
|
||||
import { isOlderThan } from './timestamp.std.js';
|
||||
import type {
|
||||
GroupSendOptionsType,
|
||||
GroupMessageOptionsType,
|
||||
SendOptionsType,
|
||||
} from '../textsecure/SendMessage.preload.js';
|
||||
import { messageSender } from '../textsecure/SendMessage.preload.js';
|
||||
@@ -137,7 +137,7 @@ export async function sendToGroup({
|
||||
}: {
|
||||
abortSignal?: AbortSignal;
|
||||
contentHint: number;
|
||||
groupSendOptions: GroupSendOptionsType;
|
||||
groupSendOptions: GroupMessageOptionsType;
|
||||
isPartialSend?: boolean;
|
||||
messageId: string | undefined;
|
||||
sendOptions?: SendOptionsType;
|
||||
@@ -931,7 +931,7 @@ export function _shouldFailSend(error: unknown, logId: string): boolean {
|
||||
}
|
||||
|
||||
function getRecipients(
|
||||
options: GroupSendOptionsType
|
||||
options: GroupMessageOptionsType
|
||||
): ReadonlyArray<ServiceIdString> {
|
||||
if (options.groupV2) {
|
||||
return options.groupV2.members;
|
||||
|
||||
Reference in New Issue
Block a user