Retry delivery and read receipts for up to 24 hours
This commit is contained in:
parent
e81821f4a6
commit
f9e98836b0
15 changed files with 316 additions and 243 deletions
|
@ -23,7 +23,8 @@ import type {
|
|||
} from './model-types.d';
|
||||
import * as Bytes from './Bytes';
|
||||
import * as Timers from './Timers';
|
||||
import type { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d';
|
||||
import type { WhatIsThis } from './window.d';
|
||||
import type { Receipt } from './types/Receipt';
|
||||
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
||||
import { SocketStatus } from './types/SocketStatus';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||
|
@ -131,6 +132,7 @@ import { ToastConversationArchived } from './components/ToastConversationArchive
|
|||
import { ToastConversationUnarchived } from './components/ToastConversationUnarchived';
|
||||
import { showToast } from './util/showToast';
|
||||
import { startInteractionMode } from './windows/startInteractionMode';
|
||||
import { deliveryReceiptsJobQueue } from './jobs/deliveryReceiptsJobQueue';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -378,59 +380,12 @@ export async function startApp(): Promise<void> {
|
|||
});
|
||||
window.Whisper.deliveryReceiptQueue.pause();
|
||||
window.Whisper.deliveryReceiptBatcher =
|
||||
window.Signal.Util.createBatcher<DeliveryReceiptBatcherItemType>({
|
||||
window.Signal.Util.createBatcher<Receipt>({
|
||||
name: 'Whisper.deliveryReceiptBatcher',
|
||||
wait: 500,
|
||||
maxSize: 100,
|
||||
processBatch: async items => {
|
||||
const byConversationId = window._.groupBy(items, item =>
|
||||
window.ConversationController.ensureContactIds({
|
||||
e164: item.source,
|
||||
uuid: item.sourceUuid,
|
||||
})
|
||||
);
|
||||
const ids = Object.keys(byConversationId);
|
||||
|
||||
for (let i = 0, max = ids.length; i < max; i += 1) {
|
||||
const conversationId = ids[i];
|
||||
const ourItems = byConversationId[conversationId];
|
||||
const timestamps = ourItems.map(item => item.timestamp);
|
||||
const messageIds = ourItems.map(item => item.messageId);
|
||||
|
||||
const c = window.ConversationController.get(conversationId);
|
||||
if (!c) {
|
||||
log.warn(
|
||||
`deliveryReceiptBatcher: Conversation ${conversationId} does not exist! ` +
|
||||
`Will not send delivery receipts for timestamps ${timestamps}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderUuid = c.get('uuid');
|
||||
const senderE164 = c.get('e164');
|
||||
|
||||
c.queueJob('sendDeliveryReceipt', async () => {
|
||||
try {
|
||||
const sendOptions = await getSendOptions(c.attributes);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await handleMessageSend(
|
||||
window.textsecure.messaging.sendDeliveryReceipt({
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamps,
|
||||
options: sendOptions,
|
||||
}),
|
||||
{ messageIds, sendType: 'deliveryReceipt' }
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Failed to send delivery receipt to ${senderE164}/${senderUuid} for timestamps ${timestamps}:`,
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
processBatch: async deliveryReceipts => {
|
||||
await deliveryReceiptsJobQueue.add({ deliveryReceipts });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
45
ts/jobs/deliveryReceiptsJobQueue.ts
Normal file
45
ts/jobs/deliveryReceiptsJobQueue.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
||||
import { receiptSchema, ReceiptType } from '../types/Receipt';
|
||||
import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers';
|
||||
|
||||
import { JobQueue } from './JobQueue';
|
||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||
|
||||
const deliveryReceiptsJobDataSchema = z.object({
|
||||
deliveryReceipts: receiptSchema.array(),
|
||||
});
|
||||
|
||||
type DeliveryReceiptsJobData = z.infer<typeof deliveryReceiptsJobDataSchema>;
|
||||
|
||||
export class DeliveryReceiptsJobQueue extends JobQueue<DeliveryReceiptsJobData> {
|
||||
protected parseData(data: unknown): DeliveryReceiptsJobData {
|
||||
return deliveryReceiptsJobDataSchema.parse(data);
|
||||
}
|
||||
|
||||
protected async run(
|
||||
{
|
||||
data,
|
||||
timestamp,
|
||||
}: Readonly<{ data: DeliveryReceiptsJobData; timestamp: number }>,
|
||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||
): Promise<void> {
|
||||
await runReceiptJob({
|
||||
attempt,
|
||||
log,
|
||||
timestamp,
|
||||
receipts: data.deliveryReceipts,
|
||||
type: ReceiptType.Delivery,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const deliveryReceiptsJobQueue = new DeliveryReceiptsJobQueue({
|
||||
store: jobQueueDatabaseStore,
|
||||
queueType: 'delivery receipts',
|
||||
maxAttempts: exponentialBackoffMaxAttempts(MAX_RETRY_TIME),
|
||||
});
|
42
ts/jobs/helpers/receiptHelpers.ts
Normal file
42
ts/jobs/helpers/receiptHelpers.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as durations from '../../util/durations';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
import type { Receipt, ReceiptType } from '../../types/Receipt';
|
||||
import { sendReceipts } from '../../util/sendReceipts';
|
||||
import { commonShouldJobContinue } from './commonShouldJobContinue';
|
||||
import { handleCommonJobRequestError } from './handleCommonJobRequestError';
|
||||
|
||||
export const MAX_RETRY_TIME = durations.DAY;
|
||||
|
||||
export async function runReceiptJob({
|
||||
attempt,
|
||||
log,
|
||||
timestamp,
|
||||
receipts,
|
||||
type,
|
||||
}: Readonly<{
|
||||
attempt: number;
|
||||
log: LoggerType;
|
||||
receipts: ReadonlyArray<Receipt>;
|
||||
timestamp: number;
|
||||
type: ReceiptType;
|
||||
}>): Promise<void> {
|
||||
const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now();
|
||||
|
||||
const shouldContinue = await commonShouldJobContinue({
|
||||
attempt,
|
||||
log,
|
||||
timeRemaining,
|
||||
});
|
||||
if (!shouldContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendReceipts({ log, receipts, type });
|
||||
} catch (err: unknown) {
|
||||
await handleCommonJobRequestError({ err, log, timeRemaining });
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
import type { WebAPIType } from '../textsecure/WebAPI';
|
||||
|
||||
import { deliveryReceiptsJobQueue } from './deliveryReceiptsJobQueue';
|
||||
import { normalMessageSendJobQueue } from './normalMessageSendJobQueue';
|
||||
import { reactionJobQueue } from './reactionJobQueue';
|
||||
import { readReceiptsJobQueue } from './readReceiptsJobQueue';
|
||||
import { readSyncJobQueue } from './readSyncJobQueue';
|
||||
import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue';
|
||||
import { reportSpamJobQueue } from './reportSpamJobQueue';
|
||||
|
@ -21,8 +23,10 @@ export function initializeAllJobQueues({
|
|||
}): void {
|
||||
reportSpamJobQueue.initialize({ server });
|
||||
|
||||
deliveryReceiptsJobQueue.streamJobs();
|
||||
normalMessageSendJobQueue.streamJobs();
|
||||
reactionJobQueue.streamJobs();
|
||||
readReceiptsJobQueue.streamJobs();
|
||||
readSyncJobQueue.streamJobs();
|
||||
removeStorageKeyJobQueue.streamJobs();
|
||||
reportSpamJobQueue.streamJobs();
|
||||
|
|
56
ts/jobs/readReceiptsJobQueue.ts
Normal file
56
ts/jobs/readReceiptsJobQueue.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
||||
import type { StorageInterface } from '../types/Storage.d';
|
||||
import type { Receipt } from '../types/Receipt';
|
||||
import { receiptSchema, ReceiptType } from '../types/Receipt';
|
||||
import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers';
|
||||
|
||||
import { JobQueue } from './JobQueue';
|
||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||
|
||||
const readReceiptsJobDataSchema = z.object({
|
||||
readReceipts: receiptSchema.array(),
|
||||
});
|
||||
|
||||
type ReadReceiptsJobData = z.infer<typeof readReceiptsJobDataSchema>;
|
||||
|
||||
export class ReadReceiptsJobQueue extends JobQueue<ReadReceiptsJobData> {
|
||||
public async addIfAllowedByUser(
|
||||
storage: Pick<StorageInterface, 'get'>,
|
||||
readReceipts: Array<Receipt>
|
||||
): Promise<void> {
|
||||
if (storage.get('read-receipt-setting')) {
|
||||
await this.add({ readReceipts });
|
||||
}
|
||||
}
|
||||
|
||||
protected parseData(data: unknown): ReadReceiptsJobData {
|
||||
return readReceiptsJobDataSchema.parse(data);
|
||||
}
|
||||
|
||||
protected async run(
|
||||
{
|
||||
data,
|
||||
timestamp,
|
||||
}: Readonly<{ data: ReadReceiptsJobData; timestamp: number }>,
|
||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||
): Promise<void> {
|
||||
await runReceiptJob({
|
||||
attempt,
|
||||
log,
|
||||
timestamp,
|
||||
receipts: data.readReceipts,
|
||||
type: ReceiptType.Read,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const readReceiptsJobQueue = new ReadReceiptsJobQueue({
|
||||
store: jobQueueDatabaseStore,
|
||||
queueType: 'read receipts',
|
||||
maxAttempts: exponentialBackoffMaxAttempts(MAX_RETRY_TIME),
|
||||
});
|
|
@ -2,26 +2,15 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as durations from '../util/durations';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
|
||||
import { commonShouldJobContinue } from './helpers/commonShouldJobContinue';
|
||||
import { sendViewedReceipt } from '../util/sendViewedReceipt';
|
||||
import { receiptSchema, ReceiptType } from '../types/Receipt';
|
||||
import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers';
|
||||
|
||||
import { JobQueue } from './JobQueue';
|
||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||
import { handleCommonJobRequestError } from './helpers/handleCommonJobRequestError';
|
||||
|
||||
const MAX_RETRY_TIME = durations.DAY;
|
||||
|
||||
const viewedReceiptsJobDataSchema = z.object({
|
||||
viewedReceipt: z.object({
|
||||
messageId: z.string(),
|
||||
senderE164: z.string().optional(),
|
||||
senderUuid: z.string().optional(),
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
});
|
||||
const viewedReceiptsJobDataSchema = z.object({ viewedReceipt: receiptSchema });
|
||||
|
||||
type ViewedReceiptsJobData = z.infer<typeof viewedReceiptsJobDataSchema>;
|
||||
|
||||
|
@ -37,22 +26,13 @@ export class ViewedReceiptsJobQueue extends JobQueue<ViewedReceiptsJobData> {
|
|||
}: Readonly<{ data: ViewedReceiptsJobData; timestamp: number }>,
|
||||
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
|
||||
): Promise<void> {
|
||||
const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now();
|
||||
|
||||
const shouldContinue = await commonShouldJobContinue({
|
||||
await runReceiptJob({
|
||||
attempt,
|
||||
log,
|
||||
timeRemaining,
|
||||
timestamp,
|
||||
receipts: [data.viewedReceipt],
|
||||
type: ReceiptType.Viewed,
|
||||
});
|
||||
if (!shouldContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendViewedReceipt(data.viewedReceipt, log);
|
||||
} catch (err: unknown) {
|
||||
await handleCommonJobRequestError({ err, log, timeRemaining });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,6 @@ import { isConversationAccepted } from '../util/isConversationAccepted';
|
|||
import { markConversationRead } from '../util/markConversationRead';
|
||||
import { handleMessageSend } from '../util/handleMessageSend';
|
||||
import { getConversationMembers } from '../util/getConversationMembers';
|
||||
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
|
||||
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
|
@ -92,6 +91,7 @@ import {
|
|||
getMessagePropStatus,
|
||||
} from '../state/selectors/message';
|
||||
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
|
||||
import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue';
|
||||
import { Deletes } from '../messageModifiers/Deletes';
|
||||
import type { ReactionModel } from '../messageModifiers/Reactions';
|
||||
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
||||
|
@ -1976,21 +1976,18 @@ export class ConversationModel extends window.Backbone
|
|||
const readMessages = messages.filter(
|
||||
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
|
||||
);
|
||||
const receiptSpecs = readMessages.map(m => ({
|
||||
messageId: m.id,
|
||||
senderE164: m.get('source'),
|
||||
senderUuid: m.get('sourceUuid'),
|
||||
senderId: window.ConversationController.ensureContactIds({
|
||||
e164: m.get('source'),
|
||||
uuid: m.get('sourceUuid'),
|
||||
}),
|
||||
timestamp: m.get('sent_at'),
|
||||
hasErrors: hasErrors(m.attributes),
|
||||
}));
|
||||
|
||||
if (isLocalAction) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sendReadReceiptsFor(this.attributes, receiptSpecs);
|
||||
await readReceiptsJobQueue.addIfAllowedByUser(
|
||||
window.storage,
|
||||
readMessages.map(m => ({
|
||||
messageId: m.id,
|
||||
senderE164: m.get('source'),
|
||||
senderUuid: m.get('sourceUuid'),
|
||||
timestamp: m.get('sent_at'),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
|
|
@ -2524,8 +2524,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
window.Whisper.deliveryReceiptQueue.add(() => {
|
||||
window.Whisper.deliveryReceiptBatcher.add({
|
||||
messageId,
|
||||
source,
|
||||
sourceUuid,
|
||||
senderE164: source,
|
||||
senderUuid: sourceUuid,
|
||||
timestamp: this.get('sent_at'),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1584,7 +1584,7 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
async sendReadReceipts(
|
||||
async sendReadReceipt(
|
||||
options: Readonly<{
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
|
@ -1598,7 +1598,7 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
async sendViewedReceipts(
|
||||
async sendViewedReceipt(
|
||||
options: Readonly<{
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
|
|
19
ts/types/Receipt.ts
Normal file
19
ts/types/Receipt.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const receiptSchema = z.object({
|
||||
messageId: z.string(),
|
||||
senderE164: z.string().optional(),
|
||||
senderUuid: z.string().optional(),
|
||||
timestamp: z.number(),
|
||||
});
|
||||
|
||||
export enum ReceiptType {
|
||||
Delivery = 'deliveryReceipt',
|
||||
Read = 'readReceipt',
|
||||
Viewed = 'viewedReceipt',
|
||||
}
|
||||
|
||||
export type Receipt = z.infer<typeof receiptSchema>;
|
|
@ -2,8 +2,8 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types.d';
|
||||
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
||||
import { hasErrors } from '../state/selectors/message';
|
||||
import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue';
|
||||
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
|
||||
import { notificationService } from '../services/notifications';
|
||||
import * as log from '../logging/log';
|
||||
|
@ -115,7 +115,10 @@ export async function markConversationRead(
|
|||
readSyncJobQueue.add({ readSyncs });
|
||||
}
|
||||
|
||||
await sendReadReceiptsFor(conversationAttrs, unreadMessagesSyncData);
|
||||
await readReceiptsJobQueue.addIfAllowedByUser(
|
||||
window.storage,
|
||||
allReadMessagesSync
|
||||
);
|
||||
}
|
||||
|
||||
window.Whisper.ExpiringMessagesListener.update();
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { chunk, groupBy, map } from 'lodash';
|
||||
import type { ConversationAttributesType } from '../model-types.d';
|
||||
import { getSendOptions } from './getSendOptions';
|
||||
import { handleMessageSend } from './handleMessageSend';
|
||||
import { isConversationAccepted } from './isConversationAccepted';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
type ReceiptSpecType = {
|
||||
messageId: string;
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
senderId?: string;
|
||||
timestamp: number;
|
||||
hasErrors: boolean;
|
||||
};
|
||||
|
||||
const CHUNK_SIZE = 100;
|
||||
|
||||
export async function sendReadReceiptsFor(
|
||||
conversationAttrs: ConversationAttributesType,
|
||||
items: Array<ReceiptSpecType>
|
||||
): Promise<void> {
|
||||
// Only send read receipts for accepted conversations
|
||||
if (
|
||||
window.Events.getReadReceiptSetting() &&
|
||||
isConversationAccepted(conversationAttrs)
|
||||
) {
|
||||
log.info(`Sending ${items.length} read receipts`);
|
||||
const sendOptions = await getSendOptions(conversationAttrs);
|
||||
const receiptsBySender = groupBy(items, 'senderId');
|
||||
|
||||
await Promise.all(
|
||||
map(receiptsBySender, async (receipts, senderId) => {
|
||||
const conversation = window.ConversationController.get(senderId);
|
||||
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batches = chunk(receipts, CHUNK_SIZE);
|
||||
await Promise.all(
|
||||
batches.map(batch => {
|
||||
const timestamps = map(batch, item => item.timestamp);
|
||||
const messageIds = map(batch, item => item.messageId);
|
||||
|
||||
return handleMessageSend(
|
||||
window.textsecure.messaging.sendReadReceipts({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
senderE164: conversation.get('e164')!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
senderUuid: conversation.get('uuid')!,
|
||||
timestamps,
|
||||
options: sendOptions,
|
||||
}),
|
||||
{ messageIds, sendType: 'readReceipt' }
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
116
ts/util/sendReceipts.ts
Normal file
116
ts/util/sendReceipts.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { chunk } from 'lodash';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import type { Receipt } from '../types/Receipt';
|
||||
import { ReceiptType } from '../types/Receipt';
|
||||
import { getSendOptions } from './getSendOptions';
|
||||
import { handleMessageSend } from './handleMessageSend';
|
||||
import { isConversationAccepted } from './isConversationAccepted';
|
||||
import { map } from './iterables';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
|
||||
const CHUNK_SIZE = 100;
|
||||
|
||||
export async function sendReceipts({
|
||||
log,
|
||||
receipts,
|
||||
type,
|
||||
}: Readonly<{
|
||||
log: LoggerType;
|
||||
receipts: ReadonlyArray<Receipt>;
|
||||
type: ReceiptType;
|
||||
}>): Promise<void> {
|
||||
let requiresUserSetting: boolean;
|
||||
let methodName:
|
||||
| 'sendDeliveryReceipt'
|
||||
| 'sendReadReceipt'
|
||||
| 'sendViewedReceipt';
|
||||
switch (type) {
|
||||
case ReceiptType.Delivery:
|
||||
requiresUserSetting = false;
|
||||
methodName = 'sendDeliveryReceipt';
|
||||
break;
|
||||
case ReceiptType.Read:
|
||||
requiresUserSetting = true;
|
||||
methodName = 'sendReadReceipt';
|
||||
break;
|
||||
case ReceiptType.Viewed:
|
||||
requiresUserSetting = true;
|
||||
methodName = 'sendViewedReceipt';
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
if (requiresUserSetting && !window.storage.get('read-receipt-setting')) {
|
||||
log.info('requires user setting. Not sending these receipts');
|
||||
return;
|
||||
}
|
||||
|
||||
const receiptsBySenderId: Map<string, Array<Receipt>> = receipts.reduce(
|
||||
(result, receipt) => {
|
||||
const { senderE164, senderUuid } = receipt;
|
||||
if (!senderE164 && !senderUuid) {
|
||||
log.error('no sender E164 or UUID. Skipping this receipt');
|
||||
return result;
|
||||
}
|
||||
|
||||
const senderId = window.ConversationController.ensureContactIds({
|
||||
e164: senderE164,
|
||||
uuid: senderUuid,
|
||||
});
|
||||
if (!senderId) {
|
||||
throw new Error(
|
||||
'no conversation found with that E164/UUID. Cannot send this receipt'
|
||||
);
|
||||
}
|
||||
|
||||
const existingGroup = result.get(senderId);
|
||||
if (existingGroup) {
|
||||
existingGroup.push(receipt);
|
||||
} else {
|
||||
result.set(senderId, [receipt]);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
new Map()
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
map(receiptsBySenderId, async ([senderId, receiptsForSender]) => {
|
||||
const sender = window.ConversationController.get(senderId);
|
||||
if (!sender) {
|
||||
throw new Error(
|
||||
'despite having a conversation ID, no conversation was found'
|
||||
);
|
||||
}
|
||||
|
||||
if (!isConversationAccepted(sender.attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendOptions = await getSendOptions(sender.attributes);
|
||||
|
||||
const batches = chunk(receiptsForSender, CHUNK_SIZE);
|
||||
await Promise.all(
|
||||
map(batches, async batch => {
|
||||
const timestamps = batch.map(receipt => receipt.timestamp);
|
||||
const messageIds = batch.map(receipt => receipt.messageId);
|
||||
|
||||
await handleMessageSend(
|
||||
window.textsecure.messaging[methodName]({
|
||||
senderE164: sender.get('e164'),
|
||||
senderUuid: sender.get('uuid'),
|
||||
timestamps,
|
||||
options: sendOptions,
|
||||
}),
|
||||
{ messageIds, sendType: type }
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types.d';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import { getSendOptions } from './getSendOptions';
|
||||
import { handleMessageSend } from './handleMessageSend';
|
||||
import { isConversationAccepted } from './isConversationAccepted';
|
||||
|
||||
export async function sendViewedReceipt(
|
||||
{
|
||||
messageId,
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
}: Readonly<{
|
||||
messageId: string;
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
timestamp: number;
|
||||
}>,
|
||||
log: LoggerType
|
||||
): Promise<void> {
|
||||
if (!window.storage.get('read-receipt-setting')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We introduced a bug in `75f0cd50beff73885ebae92e4ac977de9f56d6c9` where we'd enqueue
|
||||
// jobs that had no sender information. These jobs cannot possibly succeed. This
|
||||
// removes them from the queue to avoid constantly retrying something.
|
||||
//
|
||||
// We should be able to safely remove this check after the fix has been present for
|
||||
// awhile. Probably ~40 days from when this is first deployed (30 days to unlink + 10
|
||||
// days of buffer).
|
||||
if (!senderE164 && !senderUuid) {
|
||||
log.error(
|
||||
'sendViewedReceipt: no sender E164 or UUID. Cannot possibly complete this job. Giving up'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationId = window.ConversationController.ensureContactIds({
|
||||
e164: senderE164,
|
||||
uuid: senderUuid,
|
||||
});
|
||||
if (!conversationId) {
|
||||
throw new Error(
|
||||
'sendViewedReceipt: no conversation found with that E164/UUID'
|
||||
);
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'sendViewedReceipt: no conversation found with that conversation ID, even though we found the ID with E164/UUID?'
|
||||
);
|
||||
}
|
||||
|
||||
const conversationAttrs: ConversationAttributesType = conversation.attributes;
|
||||
if (!isConversationAccepted(conversationAttrs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handleMessageSend(
|
||||
window.textsecure.messaging.sendViewedReceipts({
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamps: [timestamp],
|
||||
options: await getSendOptions(conversationAttrs),
|
||||
}),
|
||||
{ messageIds: [messageId], sendType: 'viewedReceipt' }
|
||||
);
|
||||
}
|
10
ts/window.d.ts
vendored
10
ts/window.d.ts
vendored
|
@ -35,6 +35,7 @@ import { getEnvironment } from './environment';
|
|||
import * as zkgroup from './util/zkgroup';
|
||||
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
||||
import * as EmbeddedContact from './types/EmbeddedContact';
|
||||
import type { Receipt } from './types/Receipt';
|
||||
import * as Errors from './types/errors';
|
||||
import { ConversationController } from './ConversationController';
|
||||
import { ReduxActions } from './state/types';
|
||||
|
@ -523,13 +524,6 @@ export class CanvasVideoRenderer {
|
|||
constructor(canvas: Ref<HTMLCanvasElement>);
|
||||
}
|
||||
|
||||
export type DeliveryReceiptBatcherItemType = {
|
||||
messageId: string;
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export class AnyViewClass extends window.Backbone.View<any> {
|
||||
public headerTitle?: string;
|
||||
static show(view: typeof AnyViewClass, element: Element): void;
|
||||
|
@ -551,7 +545,7 @@ export type WhisperType = {
|
|||
WallClockListener: WhatIsThis;
|
||||
|
||||
deliveryReceiptQueue: PQueue;
|
||||
deliveryReceiptBatcher: BatcherType<DeliveryReceiptBatcherItemType>;
|
||||
deliveryReceiptBatcher: BatcherType<Receipt>;
|
||||
events: Backbone.Events;
|
||||
activeConfirmationView: WhatIsThis;
|
||||
|
||||
|
|
Loading…
Reference in a new issue