Retry delivery and read receipts for up to 24 hours

This commit is contained in:
Evan Hahn 2021-12-07 16:41:40 -06:00 committed by GitHub
parent e81821f4a6
commit f9e98836b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 316 additions and 243 deletions

View file

@ -23,7 +23,8 @@ import type {
} from './model-types.d'; } from './model-types.d';
import * as Bytes from './Bytes'; import * as Bytes from './Bytes';
import * as Timers from './Timers'; 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 { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
@ -131,6 +132,7 @@ import { ToastConversationArchived } from './components/ToastConversationArchive
import { ToastConversationUnarchived } from './components/ToastConversationUnarchived'; import { ToastConversationUnarchived } from './components/ToastConversationUnarchived';
import { showToast } from './util/showToast'; import { showToast } from './util/showToast';
import { startInteractionMode } from './windows/startInteractionMode'; import { startInteractionMode } from './windows/startInteractionMode';
import { deliveryReceiptsJobQueue } from './jobs/deliveryReceiptsJobQueue';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -378,59 +380,12 @@ export async function startApp(): Promise<void> {
}); });
window.Whisper.deliveryReceiptQueue.pause(); window.Whisper.deliveryReceiptQueue.pause();
window.Whisper.deliveryReceiptBatcher = window.Whisper.deliveryReceiptBatcher =
window.Signal.Util.createBatcher<DeliveryReceiptBatcherItemType>({ window.Signal.Util.createBatcher<Receipt>({
name: 'Whisper.deliveryReceiptBatcher', name: 'Whisper.deliveryReceiptBatcher',
wait: 500, wait: 500,
maxSize: 100, maxSize: 100,
processBatch: async items => { processBatch: async deliveryReceipts => {
const byConversationId = window._.groupBy(items, item => await deliveryReceiptsJobQueue.add({ deliveryReceipts });
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
);
}
});
}
}, },
}); });

View 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),
});

View 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 });
}
}

View file

@ -3,8 +3,10 @@
import type { WebAPIType } from '../textsecure/WebAPI'; import type { WebAPIType } from '../textsecure/WebAPI';
import { deliveryReceiptsJobQueue } from './deliveryReceiptsJobQueue';
import { normalMessageSendJobQueue } from './normalMessageSendJobQueue'; import { normalMessageSendJobQueue } from './normalMessageSendJobQueue';
import { reactionJobQueue } from './reactionJobQueue'; import { reactionJobQueue } from './reactionJobQueue';
import { readReceiptsJobQueue } from './readReceiptsJobQueue';
import { readSyncJobQueue } from './readSyncJobQueue'; import { readSyncJobQueue } from './readSyncJobQueue';
import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue'; import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue';
import { reportSpamJobQueue } from './reportSpamJobQueue'; import { reportSpamJobQueue } from './reportSpamJobQueue';
@ -21,8 +23,10 @@ export function initializeAllJobQueues({
}): void { }): void {
reportSpamJobQueue.initialize({ server }); reportSpamJobQueue.initialize({ server });
deliveryReceiptsJobQueue.streamJobs();
normalMessageSendJobQueue.streamJobs(); normalMessageSendJobQueue.streamJobs();
reactionJobQueue.streamJobs(); reactionJobQueue.streamJobs();
readReceiptsJobQueue.streamJobs();
readSyncJobQueue.streamJobs(); readSyncJobQueue.streamJobs();
removeStorageKeyJobQueue.streamJobs(); removeStorageKeyJobQueue.streamJobs();
reportSpamJobQueue.streamJobs(); reportSpamJobQueue.streamJobs();

View 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),
});

View file

@ -2,26 +2,15 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { z } from 'zod'; import { z } from 'zod';
import * as durations from '../util/durations';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff'; import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
import { commonShouldJobContinue } from './helpers/commonShouldJobContinue'; import { receiptSchema, ReceiptType } from '../types/Receipt';
import { sendViewedReceipt } from '../util/sendViewedReceipt'; import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers';
import { JobQueue } from './JobQueue'; import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { handleCommonJobRequestError } from './helpers/handleCommonJobRequestError';
const MAX_RETRY_TIME = durations.DAY; const viewedReceiptsJobDataSchema = z.object({ viewedReceipt: receiptSchema });
const viewedReceiptsJobDataSchema = z.object({
viewedReceipt: z.object({
messageId: z.string(),
senderE164: z.string().optional(),
senderUuid: z.string().optional(),
timestamp: z.number(),
}),
});
type ViewedReceiptsJobData = z.infer<typeof viewedReceiptsJobDataSchema>; type ViewedReceiptsJobData = z.infer<typeof viewedReceiptsJobDataSchema>;
@ -37,22 +26,13 @@ export class ViewedReceiptsJobQueue extends JobQueue<ViewedReceiptsJobData> {
}: Readonly<{ data: ViewedReceiptsJobData; timestamp: number }>, }: Readonly<{ data: ViewedReceiptsJobData; timestamp: number }>,
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }> { attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
): Promise<void> { ): Promise<void> {
const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now(); await runReceiptJob({
const shouldContinue = await commonShouldJobContinue({
attempt, attempt,
log, 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 });
}
} }
} }

View file

@ -62,7 +62,6 @@ import { isConversationAccepted } from '../util/isConversationAccepted';
import { markConversationRead } from '../util/markConversationRead'; import { markConversationRead } from '../util/markConversationRead';
import { handleMessageSend } from '../util/handleMessageSend'; import { handleMessageSend } from '../util/handleMessageSend';
import { getConversationMembers } from '../util/getConversationMembers'; import { getConversationMembers } from '../util/getConversationMembers';
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import { SendStatus } from '../messages/MessageSendState'; import { SendStatus } from '../messages/MessageSendState';
@ -92,6 +91,7 @@ import {
getMessagePropStatus, getMessagePropStatus,
} from '../state/selectors/message'; } from '../state/selectors/message';
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue'; import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue';
import { Deletes } from '../messageModifiers/Deletes'; import { Deletes } from '../messageModifiers/Deletes';
import type { ReactionModel } from '../messageModifiers/Reactions'; import type { ReactionModel } from '../messageModifiers/Reactions';
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady'; import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
@ -1976,21 +1976,18 @@ export class ConversationModel extends window.Backbone
const readMessages = messages.filter( const readMessages = messages.filter(
m => !hasErrors(m.attributes) && isIncoming(m.attributes) 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) { if (isLocalAction) {
// eslint-disable-next-line no-await-in-loop // 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 // eslint-disable-next-line no-await-in-loop

View file

@ -2524,8 +2524,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
window.Whisper.deliveryReceiptQueue.add(() => { window.Whisper.deliveryReceiptQueue.add(() => {
window.Whisper.deliveryReceiptBatcher.add({ window.Whisper.deliveryReceiptBatcher.add({
messageId, messageId,
source, senderE164: source,
sourceUuid, senderUuid: sourceUuid,
timestamp: this.get('sent_at'), timestamp: this.get('sent_at'),
}); });
}); });

View file

@ -1584,7 +1584,7 @@ export default class MessageSender {
}); });
} }
async sendReadReceipts( async sendReadReceipt(
options: Readonly<{ options: Readonly<{
senderE164?: string; senderE164?: string;
senderUuid?: string; senderUuid?: string;
@ -1598,7 +1598,7 @@ export default class MessageSender {
}); });
} }
async sendViewedReceipts( async sendViewedReceipt(
options: Readonly<{ options: Readonly<{
senderE164?: string; senderE164?: string;
senderUuid?: string; senderUuid?: string;

19
ts/types/Receipt.ts Normal file
View 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>;

View file

@ -2,8 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
import { hasErrors } from '../state/selectors/message'; import { hasErrors } from '../state/selectors/message';
import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue';
import { readSyncJobQueue } from '../jobs/readSyncJobQueue'; import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
import { notificationService } from '../services/notifications'; import { notificationService } from '../services/notifications';
import * as log from '../logging/log'; import * as log from '../logging/log';
@ -115,7 +115,10 @@ export async function markConversationRead(
readSyncJobQueue.add({ readSyncs }); readSyncJobQueue.add({ readSyncs });
} }
await sendReadReceiptsFor(conversationAttrs, unreadMessagesSyncData); await readReceiptsJobQueue.addIfAllowedByUser(
window.storage,
allReadMessagesSync
);
} }
window.Whisper.ExpiringMessagesListener.update(); window.Whisper.ExpiringMessagesListener.update();

View file

@ -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
View 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 }
);
})
);
})
);
}

View file

@ -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
View file

@ -35,6 +35,7 @@ import { getEnvironment } from './environment';
import * as zkgroup from './util/zkgroup'; import * as zkgroup from './util/zkgroup';
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util'; import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
import * as EmbeddedContact from './types/EmbeddedContact'; import * as EmbeddedContact from './types/EmbeddedContact';
import type { Receipt } from './types/Receipt';
import * as Errors from './types/errors'; import * as Errors from './types/errors';
import { ConversationController } from './ConversationController'; import { ConversationController } from './ConversationController';
import { ReduxActions } from './state/types'; import { ReduxActions } from './state/types';
@ -523,13 +524,6 @@ export class CanvasVideoRenderer {
constructor(canvas: Ref<HTMLCanvasElement>); constructor(canvas: Ref<HTMLCanvasElement>);
} }
export type DeliveryReceiptBatcherItemType = {
messageId: string;
source?: string;
sourceUuid?: string;
timestamp: number;
};
export class AnyViewClass extends window.Backbone.View<any> { export class AnyViewClass extends window.Backbone.View<any> {
public headerTitle?: string; public headerTitle?: string;
static show(view: typeof AnyViewClass, element: Element): void; static show(view: typeof AnyViewClass, element: Element): void;
@ -551,7 +545,7 @@ export type WhisperType = {
WallClockListener: WhatIsThis; WallClockListener: WhatIsThis;
deliveryReceiptQueue: PQueue; deliveryReceiptQueue: PQueue;
deliveryReceiptBatcher: BatcherType<DeliveryReceiptBatcherItemType>; deliveryReceiptBatcher: BatcherType<Receipt>;
events: Backbone.Events; events: Backbone.Events;
activeConfirmationView: WhatIsThis; activeConfirmationView: WhatIsThis;