Do not confirm messages until we have handled them

This commit is contained in:
Josh Perez 2023-08-21 16:08:27 -04:00 committed by GitHub
parent 29aa188c0f
commit 04f716986c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 990 additions and 960 deletions

View file

@ -7,7 +7,6 @@ import * as log from '../logging/log';
import * as Errors from '../types/errors';
import { deleteForEveryone } from '../util/deleteForEveryone';
import { drop } from '../util/drop';
import { filter, size } from '../util/iterables';
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
export type DeleteAttributesType = {
@ -20,28 +19,33 @@ export type DeleteAttributesType = {
const deletes = new Map<string, DeleteAttributesType>();
function remove(del: DeleteAttributesType): void {
del.removeFromMessageReceiverCache();
deletes.delete(del.envelopeId);
}
export function forMessage(
messageAttributes: MessageAttributesType
): Array<DeleteAttributesType> {
const sentTimestamps = getMessageSentTimestampSet(messageAttributes);
const matchingDeletes = filter(deletes, ([_envelopeId, item]) => {
const deleteValues = Array.from(deletes.values());
const matchingDeletes = deleteValues.filter(item => {
return (
item.fromId === getContactId(messageAttributes) &&
sentTimestamps.has(item.targetSentTimestamp)
);
});
if (size(matchingDeletes) > 0) {
log.info('Found early DOE for message');
const result = Array.from(matchingDeletes);
result.forEach(([envelopeId, del]) => {
del.removeFromMessageReceiverCache();
deletes.delete(envelopeId);
});
return result.map(([_envelopeId, item]) => item);
if (!matchingDeletes.length) {
return [];
}
return [];
log.info('Found early DOE for message');
matchingDeletes.forEach(del => {
remove(del);
});
return matchingDeletes;
}
export async function onDelete(del: DeleteAttributesType): Promise<void> {
@ -88,11 +92,11 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
await deleteForEveryone(message, del);
deletes.delete(del.envelopeId);
del.removeFromMessageReceiverCache();
remove(del);
})
);
} catch (error) {
remove(del);
log.error(`${logId}: error`, Errors.toLogFormat(error));
}
}

View file

@ -5,7 +5,6 @@ import type { MessageAttributesType } from '../model-types.d';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { drop } from '../util/drop';
import { filter, size } from '../util/iterables';
import { getContactId } from '../messages/helpers';
import { handleEditMessage } from '../util/handleEditMessage';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
@ -22,6 +21,11 @@ export type EditAttributesType = {
const edits = new Map<string, EditAttributesType>();
function remove(edit: EditAttributesType): void {
edits.delete(edit.envelopeId);
edit.removeFromMessageReceiverCache();
}
export function forMessage(
messageAttributes: Pick<
MessageAttributesType,
@ -34,22 +38,22 @@ export function forMessage(
>
): Array<EditAttributesType> {
const sentAt = getMessageSentTimestamp(messageAttributes, { log });
const matchingEdits = filter(edits, ([_envelopeId, item]) => {
const editValues = Array.from(edits.values());
const matchingEdits = editValues.filter(item => {
return (
item.targetSentTimestamp === sentAt &&
item.fromId === getContactId(messageAttributes)
);
});
if (size(matchingEdits) > 0) {
const result: Array<EditAttributesType> = [];
if (matchingEdits.length > 0) {
const editsLogIds: Array<number> = [];
Array.from(matchingEdits).forEach(([envelopeId, item]) => {
result.push(item);
const result = matchingEdits.map(item => {
editsLogIds.push(item.message.sent_at);
edits.delete(envelopeId);
item.removeFromMessageReceiverCache();
remove(item);
return item;
});
log.info(
@ -99,7 +103,6 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
if (!targetMessage) {
log.info(`${logId}: No message`);
return;
}
@ -110,11 +113,11 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
await handleEditMessage(message.attributes, edit);
edits.delete(edit.envelopeId);
edit.removeFromMessageReceiverCache();
remove(edit);
})
);
} catch (error) {
remove(edit);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}

View file

@ -1,10 +1,7 @@
// Copyright 2016 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { isEqual } from 'lodash';
import { Collection, Model } from 'backbone';
import type { MessageModel } from '../models/messages';
import type { MessageAttributesType } from '../model-types.d';
@ -26,6 +23,7 @@ import * as log from '../logging/log';
import { getSourceServiceId } from '../messages/helpers';
import { queueUpdateMessage } from '../util/messageBatcher';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { getMessageIdForLogging } from '../util/idForLogging';
const { deleteSentProtoRecipient } = dataInterface;
@ -36,18 +34,18 @@ export enum MessageReceiptType {
}
export type MessageReceiptAttributesType = {
envelopeId: string;
messageSentAt: number;
receiptTimestamp: number;
sourceServiceId: ServiceIdString;
removeFromMessageReceiverCache: () => unknown;
sourceConversationId: string;
sourceDevice: number;
sourceServiceId: ServiceIdString;
type: MessageReceiptType;
wasSentEncrypted: boolean;
};
class MessageReceiptModel extends Model<MessageReceiptAttributesType> {}
let singleton: MessageReceipts | undefined;
const receipts = new Map<string, MessageReceiptAttributesType>();
const deleteSentProtoBatcher = createWaitBatcher({
name: 'deleteSentProtoBatcher',
@ -79,6 +77,11 @@ const deleteSentProtoBatcher = createWaitBatcher({
},
});
function remove(receipt: MessageReceiptAttributesType): void {
receipts.delete(receipt.envelopeId);
receipt.removeFromMessageReceiverCache();
}
async function getTargetMessage(
sourceId: string,
serviceId: ServiceIdString,
@ -124,10 +127,10 @@ const wasDeliveredWithSealedSender = (
);
const shouldDropReceipt = (
receipt: MessageReceiptModel,
receipt: MessageReceiptAttributesType,
message: MessageModel
): boolean => {
const type = receipt.get('type');
const { type } = receipt;
switch (type) {
case MessageReceiptType.Delivery:
return false;
@ -143,248 +146,245 @@ const shouldDropReceipt = (
}
};
export class MessageReceipts extends Collection<MessageReceiptModel> {
static getSingleton(): MessageReceipts {
if (!singleton) {
singleton = new MessageReceipts();
}
return singleton;
export function forMessage(
message: MessageModel
): Array<MessageReceiptAttributesType> {
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
return [];
}
forMessage(message: MessageModel): Array<MessageReceiptModel> {
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
return [];
}
const logId = `MessageReceipts.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
const ourAci = window.textsecure.storage.user.getCheckedAci();
const sourceServiceId = getSourceServiceId(message.attributes);
if (ourAci !== sourceServiceId) {
return [];
}
const ourAci = window.textsecure.storage.user.getCheckedAci();
const sourceServiceId = getSourceServiceId(message.attributes);
if (ourAci !== sourceServiceId) {
return [];
}
const sentAt = getMessageSentTimestamp(message.attributes, { log });
const receipts = this.filter(
receipt => receipt.get('messageSentAt') === sentAt
);
if (receipts.length) {
log.info(`MessageReceipts: found early receipts for message ${sentAt}`);
this.remove(receipts);
}
return receipts.filter(receipt => {
if (shouldDropReceipt(receipt, message)) {
log.info(
`MessageReceipts: Dropping an early receipt ${receipt.get('type')} ` +
`for message ${sentAt}`
);
return false;
}
const receiptValues = Array.from(receipts.values());
return true;
const sentAt = getMessageSentTimestamp(message.attributes, { log });
const result = receiptValues.filter(item => item.messageSentAt === sentAt);
if (result.length > 0) {
log.info(`${logId}: found early receipts for message ${sentAt}`);
result.forEach(receipt => {
remove(receipt);
});
}
private getNewSendStateByConversationId(
oldSendStateByConversationId: SendStateByConversationId,
receipt: MessageReceiptModel
): SendStateByConversationId {
const receiptTimestamp = receipt.get('receiptTimestamp');
const sourceConversationId = receipt.get('sourceConversationId');
const type = receipt.get('type');
const oldSendState = getOwn(
oldSendStateByConversationId,
sourceConversationId
) ?? { status: SendStatus.Sent, updatedAt: undefined };
let sendActionType: SendActionType;
switch (type) {
case MessageReceiptType.Delivery:
sendActionType = SendActionType.GotDeliveryReceipt;
break;
case MessageReceiptType.Read:
sendActionType = SendActionType.GotReadReceipt;
break;
case MessageReceiptType.View:
sendActionType = SendActionType.GotViewedReceipt;
break;
default:
throw missingCaseError(type);
}
const newSendState = sendStateReducer(oldSendState, {
type: sendActionType,
updatedAt: receiptTimestamp,
});
return {
...oldSendStateByConversationId,
[sourceConversationId]: newSendState,
};
}
private async updateMessageSendState(
receipt: MessageReceiptModel,
message: MessageModel
): Promise<void> {
const messageSentAt = receipt.get('messageSentAt');
return result.filter(receipt => {
if (shouldDropReceipt(receipt, message)) {
log.info(
`MessageReceipts: Dropping a receipt ${receipt.get('type')} ` +
`for message ${messageSentAt}`
`${logId}: Dropping an early receipt ${receipt.type} for message ${sentAt}`
);
return;
return false;
}
let hasChanges = false;
return true;
});
}
const editHistory = message.get('editHistory') ?? [];
const newEditHistory = editHistory?.map(edit => {
if (messageSentAt !== edit.timestamp) {
return edit;
}
function getNewSendStateByConversationId(
oldSendStateByConversationId: SendStateByConversationId,
receipt: MessageReceiptAttributesType
): SendStateByConversationId {
const { receiptTimestamp, sourceConversationId, type } = receipt;
const oldSendStateByConversationId = edit.sendStateByConversationId ?? {};
const newSendStateByConversationId = this.getNewSendStateByConversationId(
oldSendStateByConversationId,
receipt
);
const oldSendState = getOwn(
oldSendStateByConversationId,
sourceConversationId
) ?? { status: SendStatus.Sent, updatedAt: undefined };
return {
...edit,
sendStateByConversationId: newSendStateByConversationId,
};
});
if (!isEqual(newEditHistory, editHistory)) {
message.set('editHistory', newEditHistory);
let sendActionType: SendActionType;
switch (type) {
case MessageReceiptType.Delivery:
sendActionType = SendActionType.GotDeliveryReceipt;
break;
case MessageReceiptType.Read:
sendActionType = SendActionType.GotReadReceipt;
break;
case MessageReceiptType.View:
sendActionType = SendActionType.GotViewedReceipt;
break;
default:
throw missingCaseError(type);
}
const newSendState = sendStateReducer(oldSendState, {
type: sendActionType,
updatedAt: receiptTimestamp,
});
return {
...oldSendStateByConversationId,
[sourceConversationId]: newSendState,
};
}
async function updateMessageSendState(
receipt: MessageReceiptAttributesType,
message: MessageModel
): Promise<void> {
const { messageSentAt } = receipt;
const logId = `MessageReceipts.updateMessageSendState(sentAt=${receipt.messageSentAt})`;
if (shouldDropReceipt(receipt, message)) {
log.info(
`${logId}: Dropping a receipt ${receipt.type} for message ${messageSentAt}`
);
return;
}
let hasChanges = false;
const editHistory = message.get('editHistory') ?? [];
const newEditHistory = editHistory?.map(edit => {
if (messageSentAt !== edit.timestamp) {
return edit;
}
const oldSendStateByConversationId = edit.sendStateByConversationId ?? {};
const newSendStateByConversationId = getNewSendStateByConversationId(
oldSendStateByConversationId,
receipt
);
return {
...edit,
sendStateByConversationId: newSendStateByConversationId,
};
});
if (!isEqual(newEditHistory, editHistory)) {
message.set('editHistory', newEditHistory);
hasChanges = true;
}
const editMessageTimestamp = message.get('editMessageTimestamp');
if (
messageSentAt === message.get('timestamp') ||
messageSentAt === editMessageTimestamp
) {
const oldSendStateByConversationId =
message.get('sendStateByConversationId') ?? {};
const newSendStateByConversationId = getNewSendStateByConversationId(
oldSendStateByConversationId,
receipt
);
// The send state may not change. For example, this can happen if we get a read
// receipt before a delivery receipt.
if (!isEqual(oldSendStateByConversationId, newSendStateByConversationId)) {
message.set('sendStateByConversationId', newSendStateByConversationId);
hasChanges = true;
}
const editMessageTimestamp = message.get('editMessageTimestamp');
if (
messageSentAt === message.get('timestamp') ||
messageSentAt === editMessageTimestamp
) {
const oldSendStateByConversationId =
message.get('sendStateByConversationId') ?? {};
const newSendStateByConversationId = this.getNewSendStateByConversationId(
oldSendStateByConversationId,
receipt
);
// The send state may not change. For example, this can happen if we get a read
// receipt before a delivery receipt.
if (
!isEqual(oldSendStateByConversationId, newSendStateByConversationId)
) {
message.set('sendStateByConversationId', newSendStateByConversationId);
hasChanges = true;
}
}
if (hasChanges) {
queueUpdateMessage(message.attributes);
// notify frontend listeners
const conversation = window.ConversationController.get(
message.get('conversationId')
);
const updateLeftPane = conversation
? conversation.debouncedUpdateLastMessage
: undefined;
if (updateLeftPane) {
updateLeftPane();
}
}
const sourceConversationId = receipt.get('sourceConversationId');
const type = receipt.get('type');
if (
(type === MessageReceiptType.Delivery &&
wasDeliveredWithSealedSender(sourceConversationId, message) &&
receipt.get('wasSentEncrypted')) ||
type === MessageReceiptType.Read
) {
const recipient = window.ConversationController.get(sourceConversationId);
const recipientServiceId = recipient?.getServiceId();
const deviceId = receipt.get('sourceDevice');
if (recipientServiceId && deviceId) {
await Promise.all([
deleteSentProtoBatcher.add({
timestamp: messageSentAt,
recipientServiceId,
deviceId,
}),
// We want the above call to not be delayed when testing with
// CI.
window.SignalCI
? deleteSentProtoBatcher.flushAndWait()
: Promise.resolve(),
]);
} else {
log.warn(
`MessageReceipts.onReceipt: Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
);
}
}
}
async onReceipt(receipt: MessageReceiptModel): Promise<void> {
const messageSentAt = receipt.get('messageSentAt');
const sourceConversationId = receipt.get('sourceConversationId');
const sourceServiceId = receipt.get('sourceServiceId');
const type = receipt.get('type');
if (hasChanges) {
queueUpdateMessage(message.attributes);
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
messageSentAt
// notify frontend listeners
const conversation = window.ConversationController.get(
message.get('conversationId')
);
const updateLeftPane = conversation
? conversation.debouncedUpdateLastMessage
: undefined;
if (updateLeftPane) {
updateLeftPane();
}
}
const { sourceConversationId, type } = receipt;
if (
(type === MessageReceiptType.Delivery &&
wasDeliveredWithSealedSender(sourceConversationId, message) &&
receipt.wasSentEncrypted) ||
type === MessageReceiptType.Read
) {
const recipient = window.ConversationController.get(sourceConversationId);
const recipientServiceId = recipient?.getServiceId();
const deviceId = receipt.sourceDevice;
if (recipientServiceId && deviceId) {
await Promise.all([
deleteSentProtoBatcher.add({
timestamp: messageSentAt,
recipientServiceId,
deviceId,
}),
// We want the above call to not be delayed when testing with
// CI.
window.SignalCI
? deleteSentProtoBatcher.flushAndWait()
: Promise.resolve(),
]);
} else {
log.warn(
`${logId}: Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
);
const message = await getTargetMessage(
sourceConversationId,
sourceServiceId,
messages
);
if (message) {
await this.updateMessageSendState(receipt, message);
} else {
// We didn't find any messages but maybe it's a story sent message
const targetMessages = messages.filter(
item =>
item.storyDistributionListId &&
item.sendStateByConversationId &&
!item.deletedForEveryone &&
Boolean(item.sendStateByConversationId[sourceConversationId])
);
// Nope, no target message was found
if (!targetMessages.length) {
log.info(
'MessageReceipts: No message for receipt',
type,
sourceConversationId,
sourceServiceId,
messageSentAt
);
return;
}
await Promise.all(
targetMessages.map(msg => {
const model = window.MessageController.register(msg.id, msg);
return this.updateMessageSendState(receipt, model);
})
);
}
this.remove(receipt);
} catch (error) {
log.error('MessageReceipts.onReceipt error:', Errors.toLogFormat(error));
}
}
}
export async function onReceipt(
receipt: MessageReceiptAttributesType
): Promise<void> {
receipts.set(receipt.envelopeId, receipt);
const { messageSentAt, sourceConversationId, sourceServiceId, type } =
receipt;
const logId = `MessageReceipts.onReceipt(sentAt=${receipt.messageSentAt})`;
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
messageSentAt
);
const message = await getTargetMessage(
sourceConversationId,
sourceServiceId,
messages
);
if (message) {
await updateMessageSendState(receipt, message);
} else {
// We didn't find any messages but maybe it's a story sent message
const targetMessages = messages.filter(
item =>
item.storyDistributionListId &&
item.sendStateByConversationId &&
!item.deletedForEveryone &&
Boolean(item.sendStateByConversationId[sourceConversationId])
);
// Nope, no target message was found
if (!targetMessages.length) {
log.info(
`${logId}: No message for receipt`,
type,
sourceConversationId,
sourceServiceId
);
return;
}
await Promise.all(
targetMessages.map(msg => {
const model = window.MessageController.register(msg.id, msg);
return updateMessageSendState(receipt, model);
})
);
}
remove(receipt);
} catch (error) {
remove(receipt);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}

View file

@ -1,112 +1,115 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import type { ConversationModel } from '../models/conversations';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import type { AciString } from '../types/ServiceId';
import type { ConversationModel } from '../models/conversations';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { drop } from '../util/drop';
import { getConversationIdForLogging } from '../util/idForLogging';
export type MessageRequestAttributesType = {
threadE164?: string;
threadAci?: AciString;
envelopeId: string;
groupV2Id?: string;
removeFromMessageReceiverCache: () => unknown;
threadAci?: AciString;
threadE164?: string;
type: number;
};
class MessageRequestModel extends Model<MessageRequestAttributesType> {}
const messageRequests = new Map<string, MessageRequestAttributesType>();
let singleton: MessageRequests | undefined;
function remove(sync: MessageRequestAttributesType): void {
messageRequests.delete(sync.envelopeId);
sync.removeFromMessageReceiverCache();
}
export class MessageRequests extends Collection<MessageRequestModel> {
static getSingleton(): MessageRequests {
if (!singleton) {
singleton = new MessageRequests();
export function forConversation(
conversation: ConversationModel
): MessageRequestAttributesType | null {
const logId = `MessageRequests.forConversation(${getConversationIdForLogging(
conversation.attributes
)})`;
const messageRequestValues = Array.from(messageRequests.values());
if (conversation.get('e164')) {
const syncByE164 = messageRequestValues.find(
item => item.threadE164 === conversation.get('e164')
);
if (syncByE164) {
log.info(`${logId}: Found early message request response for E164`);
remove(syncByE164);
return syncByE164;
}
return singleton;
}
forConversation(conversation: ConversationModel): MessageRequestModel | null {
if (conversation.get('e164')) {
const syncByE164 = this.findWhere({
threadE164: conversation.get('e164'),
});
if (syncByE164) {
log.info(
`Found early message request response for E164 ${conversation.idForLogging()}`
);
this.remove(syncByE164);
return syncByE164;
}
if (conversation.getServiceId()) {
const syncByServiceId = messageRequestValues.find(
item => item.threadAci === conversation.getServiceId()
);
if (syncByServiceId) {
log.info(`${logId}: Found early message request response for serviceId`);
remove(syncByServiceId);
return syncByServiceId;
}
if (conversation.getServiceId()) {
const syncByAci = this.findWhere({
threadAci: conversation.getServiceId(),
});
if (syncByAci) {
log.info(
`Found early message request response for aci ${conversation.idForLogging()}`
);
this.remove(syncByAci);
return syncByAci;
}
}
// V2 group
if (conversation.get('groupId')) {
const syncByGroupId = this.findWhere({
groupV2Id: conversation.get('groupId'),
});
if (syncByGroupId) {
log.info(
`Found early message request response for group v2 ID ${conversation.idForLogging()}`
);
this.remove(syncByGroupId);
return syncByGroupId;
}
}
return null;
}
async onResponse(sync: MessageRequestModel): Promise<void> {
try {
const threadE164 = sync.get('threadE164');
const threadAci = sync.get('threadAci');
const groupV2Id = sync.get('groupV2Id');
// V2 group
if (conversation.get('groupId')) {
const syncByGroupId = messageRequestValues.find(
item => item.groupV2Id === conversation.get('groupId')
);
if (syncByGroupId) {
log.info(`${logId}: Found early message request response for gv2`);
remove(syncByGroupId);
return syncByGroupId;
}
}
let conversation;
return null;
}
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
if (groupV2Id) {
conversation = window.ConversationController.get(groupV2Id);
}
if (!conversation && (threadE164 || threadAci)) {
conversation = window.ConversationController.lookupOrCreate({
e164: threadE164,
serviceId: threadAci,
reason: 'MessageRequests.onResponse',
});
}
export async function onResponse(
sync: MessageRequestAttributesType
): Promise<void> {
messageRequests.set(sync.envelopeId, sync);
const { threadE164, threadAci, groupV2Id } = sync;
if (!conversation) {
log.warn(
`Received message request response for unknown conversation: groupv2(${groupV2Id}) ${threadAci} ${threadE164}`
);
return;
}
const logId = `MessageRequests.onResponse(groupv2(${groupV2Id}) ${threadAci} ${threadE164})`;
void conversation.applyMessageRequestResponse(sync.get('type'), {
try {
let conversation;
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
if (groupV2Id) {
conversation = window.ConversationController.get(groupV2Id);
}
if (!conversation && (threadE164 || threadAci)) {
conversation = window.ConversationController.lookupOrCreate({
e164: threadE164,
serviceId: threadAci,
reason: logId,
});
}
if (!conversation) {
log.warn(
`${logId}: received message request response for unknown conversation`
);
remove(sync);
return;
}
drop(
conversation.applyMessageRequestResponse(sync.type, {
fromSync: true,
});
})
);
this.remove(sync);
} catch (error) {
log.error('MessageRequests.onResponse error:', Errors.toLogFormat(error));
}
remove(sync);
} catch (error) {
remove(sync);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}

View file

@ -1,197 +1,220 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import type { AciString } from '../types/ServiceId';
import type { ConversationModel } from '../models/conversations';
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import type {
MessageAttributesType,
ReactionAttributesType,
} from '../model-types.d';
import type { ReactionSource } from '../reactions/ReactionSource';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { getContactId, getContact } from '../messages/helpers';
import { getMessageIdForLogging } from '../util/idForLogging';
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
import { isOutgoing, isStory } from '../state/selectors/message';
import { strictAssert } from '../util/assert';
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
export class ReactionModel extends Model<ReactionAttributesType> {}
export type ReactionAttributesType = {
emoji: string;
envelopeId: string;
fromId: string;
remove?: boolean;
removeFromMessageReceiverCache: () => unknown;
source: ReactionSource;
// Necessary to put 1:1 story replies into the right conversation - not the same
// conversation as the target message!
storyReactionMessage?: MessageModel;
targetAuthorAci: AciString;
targetTimestamp: number;
timestamp: number;
};
let singleton: Reactions | undefined;
const reactions = new Map<string, ReactionAttributesType>();
export class Reactions extends Collection<ReactionModel> {
static getSingleton(): Reactions {
if (!singleton) {
singleton = new Reactions();
}
function remove(reaction: ReactionAttributesType): void {
reactions.delete(reaction.envelopeId);
reaction.removeFromMessageReceiverCache();
}
return singleton;
}
export function forMessage(
message: MessageModel
): Array<ReactionAttributesType> {
const logId = `Reactions.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
forMessage(message: MessageModel): Array<ReactionModel> {
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
if (isOutgoing(message.attributes)) {
const outgoingReactions = this.filter(item =>
sentTimestamps.has(item.get('targetTimestamp'))
);
if (outgoingReactions.length > 0) {
log.info('Found early reaction for outgoing message');
this.remove(outgoingReactions);
return outgoingReactions;
}
}
const senderId = getContactId(message.attributes);
const reactionsBySource = this.filter(re => {
const targetSender = window.ConversationController.lookupOrCreate({
serviceId: re.get('targetAuthorAci'),
reason: 'Reactions.forMessage',
});
const targetTimestamp = re.get('targetTimestamp');
return (
targetSender?.id === senderId && sentTimestamps.has(targetTimestamp)
);
});
if (reactionsBySource.length > 0) {
log.info('Found early reaction for message');
this.remove(reactionsBySource);
return reactionsBySource;
}
return [];
}
private async findMessage(
targetTimestamp: number,
targetConversationId: string
): Promise<MessageAttributesType | undefined> {
const messages = await window.Signal.Data.getMessagesBySentAt(
targetTimestamp
const reactionValues = Array.from(reactions.values());
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
if (isOutgoing(message.attributes)) {
const outgoingReactions = reactionValues.filter(item =>
sentTimestamps.has(item.targetTimestamp)
);
return messages.find(m => {
const contact = getContact(m);
if (!contact) {
return false;
}
const mcid = contact.get('id');
return mcid === targetConversationId;
});
if (outgoingReactions.length > 0) {
log.info(`${logId}: Found early reaction for outgoing message`);
outgoingReactions.forEach(item => {
remove(item);
});
return outgoingReactions;
}
}
async onReaction(reaction: ReactionModel): Promise<void> {
try {
// The conversation the target message was in; we have to find it in the database
// to to figure that out.
const targetAuthorConversation =
window.ConversationController.lookupOrCreate({
serviceId: reaction.get('targetAuthorAci'),
reason: 'Reactions.onReaction',
});
const targetConversationId = targetAuthorConversation?.id;
if (!targetConversationId) {
throw new Error(
'onReaction: No conversationId returned from lookupOrCreate!'
const senderId = getContactId(message.attributes);
const reactionsBySource = reactionValues.filter(re => {
const targetSender = window.ConversationController.lookupOrCreate({
serviceId: re.targetAuthorAci,
reason: logId,
});
return (
targetSender?.id === senderId && sentTimestamps.has(re.targetTimestamp)
);
});
if (reactionsBySource.length > 0) {
log.info(`${logId}: Found early reaction for message`);
reactionsBySource.forEach(item => {
remove(item);
item.removeFromMessageReceiverCache();
});
return reactionsBySource;
}
return [];
}
async function findMessage(
targetTimestamp: number,
targetConversationId: string
): Promise<MessageAttributesType | undefined> {
const messages = await window.Signal.Data.getMessagesBySentAt(
targetTimestamp
);
return messages.find(m => {
const contact = getContact(m);
if (!contact) {
return false;
}
const mcid = contact.get('id');
return mcid === targetConversationId;
});
}
export async function onReaction(
reaction: ReactionAttributesType
): Promise<void> {
reactions.set(reaction.envelopeId, reaction);
const logId = `Reactions.onReaction(timestamp=${reaction.timestamp};target=${reaction.targetTimestamp})`;
try {
// The conversation the target message was in; we have to find it in the database
// to to figure that out.
const targetAuthorConversation =
window.ConversationController.lookupOrCreate({
serviceId: reaction.targetAuthorAci,
reason: logId,
});
const targetConversationId = targetAuthorConversation?.id;
if (!targetConversationId) {
throw new Error(
`${logId} Error: No conversationId returned from lookupOrCreate!`
);
}
const generatedMessage = reaction.storyReactionMessage;
strictAssert(
generatedMessage,
`${logId} strictAssert: Story reactions must provide storyReactionMessage`
);
const fromConversation = window.ConversationController.get(
generatedMessage.get('conversationId')
);
let targetConversation: ConversationModel | undefined | null;
const targetMessageCheck = await findMessage(
reaction.targetTimestamp,
targetConversationId
);
if (!targetMessageCheck) {
log.info(
`${logId}: No message for reaction`,
'targeting',
reaction.targetAuthorAci
);
return;
}
if (
fromConversation &&
isStory(targetMessageCheck) &&
isDirectConversation(fromConversation.attributes) &&
!isMe(fromConversation.attributes)
) {
targetConversation = fromConversation;
} else {
targetConversation =
await window.ConversationController.getConversationForTargetMessage(
targetConversationId,
reaction.targetTimestamp
);
}
}
const generatedMessage = reaction.get('storyReactionMessage');
strictAssert(
generatedMessage,
'Story reactions must provide storyReactionMessage'
);
const fromConversation = window.ConversationController.get(
generatedMessage.get('conversationId')
if (!targetConversation) {
log.info(
`${logId}: No target conversation for reaction`,
reaction.targetAuthorAci,
reaction.targetTimestamp
);
remove(reaction);
return undefined;
}
let targetConversation: ConversationModel | undefined | null;
const targetMessageCheck = await this.findMessage(
reaction.get('targetTimestamp'),
targetConversationId
);
if (!targetMessageCheck) {
log.info(
'No message for reaction',
reaction.get('timestamp'),
'targeting',
reaction.get('targetAuthorAci'),
reaction.get('targetTimestamp')
);
// awaiting is safe since `onReaction` is never called from inside the queue
await targetConversation.queueJob('Reactions.onReaction', async () => {
log.info(`${logId}: handling`);
// Thanks TS.
if (!targetConversation) {
remove(reaction);
return;
}
if (
fromConversation &&
isStory(targetMessageCheck) &&
isDirectConversation(fromConversation.attributes) &&
!isMe(fromConversation.attributes)
) {
targetConversation = fromConversation;
// Message is fetched inside the conversation queue so we have the
// most recent data
const targetMessage = await findMessage(
reaction.targetTimestamp,
targetConversationId
);
if (!targetMessage) {
remove(reaction);
return;
}
const message = window.MessageController.register(
targetMessage.id,
targetMessage
);
// Use the generated message in ts/background.ts to create a message
// if the reaction is targeted at a story.
if (!isStory(targetMessage)) {
await message.handleReaction(reaction);
} else {
targetConversation =
await window.ConversationController.getConversationForTargetMessage(
targetConversationId,
reaction.get('targetTimestamp')
);
await generatedMessage.handleReaction(reaction, {
storyMessage: targetMessage,
});
}
if (!targetConversation) {
log.info(
'No target conversation for reaction',
reaction.get('targetAuthorAci'),
reaction.get('targetTimestamp')
);
return undefined;
}
// awaiting is safe since `onReaction` is never called from inside the queue
await targetConversation.queueJob('Reactions.onReaction', async () => {
log.info('Handling reaction for', reaction.get('targetTimestamp'));
// Thanks TS.
if (!targetConversation) {
return;
}
// Message is fetched inside the conversation queue so we have the
// most recent data
const targetMessage = await this.findMessage(
reaction.get('targetTimestamp'),
targetConversationId
);
if (!targetMessage) {
return;
}
const message = window.MessageController.register(
targetMessage.id,
targetMessage
);
// Use the generated message in ts/background.ts to create a message
// if the reaction is targeted at a story.
if (!isStory(targetMessage)) {
await message.handleReaction(reaction);
} else {
await generatedMessage.handleReaction(reaction, {
storyMessage: targetMessage,
});
}
this.remove(reaction);
});
} catch (error) {
log.error('Reactions.onReaction error:', Errors.toLogFormat(error));
}
remove(reaction);
});
} catch (error) {
remove(reaction);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}

View file

@ -1,50 +1,52 @@
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import type { AciString } from '../types/ServiceId';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { StartupQueue } from '../util/StartupQueue';
import { getMessageIdForLogging } from '../util/idForLogging';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { isIncoming } from '../state/selectors/message';
import { isMessageUnread } from '../util/isMessageUnread';
import { notificationService } from '../services/notifications';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import type { AciString } from '../types/ServiceId';
import { StartupQueue } from '../util/StartupQueue';
import { queueUpdateMessage } from '../util/messageBatcher';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
export type ReadSyncAttributesType = {
senderId: string;
envelopeId: string;
readAt: number;
removeFromMessageReceiverCache: () => unknown;
sender?: string;
senderAci: AciString;
senderId: string;
timestamp: number;
readAt: number;
};
class ReadSyncModel extends Model<ReadSyncAttributesType> {}
const readSyncs = new Map<string, ReadSyncAttributesType>();
let singleton: ReadSyncs | undefined;
function remove(sync: ReadSyncAttributesType): void {
readSyncs.delete(sync.envelopeId);
sync.removeFromMessageReceiverCache();
}
async function maybeItIsAReactionReadSync(
sync: ReadSyncAttributesType
): Promise<void> {
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
const readReaction = await window.Signal.Data.markReactionAsRead(
sync.get('senderAci'),
Number(sync.get('timestamp'))
sync.senderAci,
Number(sync.timestamp)
);
if (!readReaction) {
log.info(
'Nothing found for read sync',
sync.get('senderId'),
sync.get('sender'),
sync.get('senderAci'),
sync.get('timestamp')
);
log.info(`${logId} not found:`, sync.senderId, sync.sender, sync.senderAci);
return;
}
remove(sync);
notificationService.removeBy({
conversationId: readReaction.conversationId,
emoji: readReaction.emoji,
@ -53,109 +55,110 @@ async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
});
}
export class ReadSyncs extends Collection {
static getSingleton(): ReadSyncs {
if (!singleton) {
singleton = new ReadSyncs();
}
export function forMessage(
message: MessageModel
): ReadSyncAttributesType | null {
const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
return singleton;
const sender = window.ConversationController.lookupOrCreate({
e164: message.get('source'),
serviceId: message.get('sourceServiceId'),
reason: logId,
});
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
log,
});
const readSyncValues = Array.from(readSyncs.values());
const foundSync = readSyncValues.find(item => {
return item.senderId === sender?.id && item.timestamp === messageTimestamp;
});
if (foundSync) {
log.info(
`${logId}: Found early read sync for message ${foundSync.timestamp}`
);
remove(foundSync);
return foundSync;
}
forMessage(message: MessageModel): ReadSyncModel | null {
const sender = window.ConversationController.lookupOrCreate({
e164: message.get('source'),
serviceId: message.get('sourceServiceId'),
reason: 'ReadSyncs.forMessage',
});
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
log,
});
const sync = this.find(item => {
return (
item.get('senderId') === sender?.id &&
item.get('timestamp') === messageTimestamp
);
});
if (sync) {
log.info(`Found early read sync for message ${sync.get('timestamp')}`);
this.remove(sync);
return sync;
}
return null;
}
return null;
}
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
readSyncs.set(sync.envelopeId, sync);
async onSync(sync: ReadSyncModel): Promise<void> {
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.get('timestamp')
);
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
const found = messages.find(item => {
const sender = window.ConversationController.lookupOrCreate({
e164: item.source,
serviceId: item.sourceServiceId,
reason: 'ReadSyncs.onSync',
});
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.timestamp
);
return isIncoming(item) && sender?.id === sync.get('senderId');
const found = messages.find(item => {
const sender = window.ConversationController.lookupOrCreate({
e164: item.source,
serviceId: item.sourceServiceId,
reason: logId,
});
if (!found) {
await maybeItIsAReactionReadSync(sync);
return;
}
return isIncoming(item) && sender?.id === sync.senderId;
});
notificationService.removeBy({ messageId: found.id });
if (!found) {
await maybeItIsAReactionReadSync(sync);
return;
}
const message = window.MessageController.register(found.id, found);
const readAt = Math.min(sync.get('readAt'), Date.now());
notificationService.removeBy({ messageId: found.id });
// If message is unread, we mark it read. Otherwise, we update the expiration
// timer to the time specified by the read sync if it's earlier than
// the previous read time.
if (isMessageUnread(message.attributes)) {
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
message.markRead(readAt, { skipSave: true });
const message = window.MessageController.register(found.id, found);
const readAt = Math.min(sync.readAt, Date.now());
const updateConversation = async () => {
// onReadMessage may result in messages older than this one being
// marked read. We want those messages to have the same expire timer
// start time as this one, so we pass the readAt value through.
void message.getConversation()?.onReadMessage(message, readAt);
};
// If message is unread, we mark it read. Otherwise, we update the expiration
// timer to the time specified by the read sync if it's earlier than
// the previous read time.
if (isMessageUnread(message.attributes)) {
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
message.markRead(readAt, { skipSave: true });
// only available during initialization
if (StartupQueue.isAvailable()) {
const conversation = message.getConversation();
if (conversation) {
StartupQueue.add(
conversation.get('id'),
message.get('sent_at'),
updateConversation
);
}
} else {
// not awaiting since we don't want to block work happening in the
// eventHandlerQueue
void updateConversation();
const updateConversation = async () => {
// onReadMessage may result in messages older than this one being
// marked read. We want those messages to have the same expire timer
// start time as this one, so we pass the readAt value through.
void message.getConversation()?.onReadMessage(message, readAt);
};
// only available during initialization
if (StartupQueue.isAvailable()) {
const conversation = message.getConversation();
if (conversation) {
StartupQueue.add(
conversation.get('id'),
message.get('sent_at'),
updateConversation
);
}
} else {
const now = Date.now();
const existingTimestamp = message.get('expirationStartTimestamp');
const expirationStartTimestamp = Math.min(
now,
Math.min(existingTimestamp || now, readAt || now)
);
message.set({ expirationStartTimestamp });
// not awaiting since we don't want to block work happening in the
// eventHandlerQueue
void updateConversation();
}
queueUpdateMessage(message.attributes);
this.remove(sync);
} catch (error) {
log.error('ReadSyncs.onSync error:', Errors.toLogFormat(error));
} else {
const now = Date.now();
const existingTimestamp = message.get('expirationStartTimestamp');
const expirationStartTimestamp = Math.min(
now,
Math.min(existingTimestamp || now, readAt || now)
);
message.set({ expirationStartTimestamp });
}
queueUpdateMessage(message.attributes);
remove(sync);
} catch (error) {
remove(sync);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}

View file

@ -1,100 +1,108 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import type { MessageModel } from '../models/messages';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import type { AciString } from '../types/ServiceId';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { getMessageIdForLogging } from '../util/idForLogging';
export type ViewOnceOpenSyncAttributesType = {
removeFromMessageReceiverCache: () => unknown;
source?: string;
sourceAci: AciString;
timestamp: number;
};
class ViewOnceOpenSyncModel extends Model<ViewOnceOpenSyncAttributesType> {}
const viewOnceSyncs = new Map<number, ViewOnceOpenSyncAttributesType>();
let singleton: ViewOnceOpenSyncs | undefined;
function remove(sync: ViewOnceOpenSyncAttributesType): void {
viewOnceSyncs.delete(sync.timestamp);
sync.removeFromMessageReceiverCache();
}
export class ViewOnceOpenSyncs extends Collection<ViewOnceOpenSyncModel> {
static getSingleton(): ViewOnceOpenSyncs {
if (!singleton) {
singleton = new ViewOnceOpenSyncs();
}
export function forMessage(
message: MessageModel
): ViewOnceOpenSyncAttributesType | null {
const logId = `ViewOnceOpenSyncs.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
return singleton;
const viewOnceSyncValues = Array.from(viewOnceSyncs.values());
const syncBySourceServiceId = viewOnceSyncValues.find(item => {
return (
item.sourceAci === message.get('sourceServiceId') &&
item.timestamp === message.get('sent_at')
);
});
if (syncBySourceServiceId) {
log.info(`${logId}: Found early view once open sync for message`);
remove(syncBySourceServiceId);
return syncBySourceServiceId;
}
forMessage(message: MessageModel): ViewOnceOpenSyncModel | null {
const syncBySourceAci = this.find(item => {
return (
item.get('sourceAci') === message.get('sourceServiceId') &&
item.get('timestamp') === message.get('sent_at')
);
});
if (syncBySourceAci) {
log.info('Found early view once open sync for message');
this.remove(syncBySourceAci);
return syncBySourceAci;
}
const syncBySource = this.find(item => {
return (
item.get('source') === message.get('source') &&
item.get('timestamp') === message.get('sent_at')
);
});
if (syncBySource) {
log.info('Found early view once open sync for message');
this.remove(syncBySource);
return syncBySource;
}
return null;
const syncBySource = viewOnceSyncValues.find(item => {
return (
item.source === message.get('source') &&
item.timestamp === message.get('sent_at')
);
});
if (syncBySource) {
log.info(`${logId}: Found early view once open sync for message`);
remove(syncBySource);
return syncBySource;
}
async onSync(sync: ViewOnceOpenSyncModel): Promise<void> {
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.get('timestamp')
return null;
}
export async function onSync(
sync: ViewOnceOpenSyncAttributesType
): Promise<void> {
viewOnceSyncs.set(sync.timestamp, sync);
const logId = `ViewOnceOpenSyncs.onSync(timestamp=${sync.timestamp})`;
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.timestamp
);
const found = messages.find(item => {
const itemSourceAci = item.sourceServiceId;
const syncSourceAci = sync.sourceAci;
const itemSource = item.source;
const syncSource = sync.source;
return Boolean(
(itemSourceAci && syncSourceAci && itemSourceAci === syncSourceAci) ||
(itemSource && syncSource && itemSource === syncSource)
);
});
const found = messages.find(item => {
const itemSourceAci = item.sourceServiceId;
const syncSourceAci = sync.get('sourceAci');
const itemSource = item.source;
const syncSource = sync.get('source');
const syncSource = sync.source;
const syncSourceAci = sync.sourceAci;
const syncTimestamp = sync.timestamp;
const wasMessageFound = Boolean(found);
log.info(`${logId} receive:`, {
syncSource,
syncSourceAci,
syncTimestamp,
wasMessageFound,
});
return Boolean(
(itemSourceAci && syncSourceAci && itemSourceAci === syncSourceAci) ||
(itemSource && syncSource && itemSource === syncSource)
);
});
const syncSource = sync.get('source');
const syncSourceAci = sync.get('sourceAci');
const syncTimestamp = sync.get('timestamp');
const wasMessageFound = Boolean(found);
log.info('Receive view once open sync:', {
syncSource,
syncSourceAci,
syncTimestamp,
wasMessageFound,
});
if (!found) {
return;
}
const message = window.MessageController.register(found.id, found);
await message.markViewOnceMessageViewed({ fromSync: true });
this.remove(sync);
} catch (error) {
log.error('ViewOnceOpenSyncs.onSync error:', Errors.toLogFormat(error));
if (!found) {
return;
}
const message = window.MessageController.register(found.id, found);
await message.markViewOnceMessageViewed({ fromSync: true });
viewOnceSyncs.delete(sync.timestamp);
sync.removeFromMessageReceiverCache();
} catch (error) {
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}

View file

@ -1,136 +1,142 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import type { MessageModel } from '../models/messages';
import { ReadStatus } from '../messages/MessageReadStatus';
import { markViewed } from '../services/MessageUpdater';
import { isDownloaded } from '../types/Attachment';
import type { AciString } from '../types/ServiceId';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import { isIncoming } from '../state/selectors/message';
import { notificationService } from '../services/notifications';
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import * as log from '../logging/log';
import { GiftBadgeStates } from '../components/conversation/Message';
import { queueUpdateMessage } from '../util/messageBatcher';
import { ReadStatus } from '../messages/MessageReadStatus';
import { getMessageIdForLogging } from '../util/idForLogging';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { isDownloaded } from '../types/Attachment';
import { isIncoming } from '../state/selectors/message';
import { markViewed } from '../services/MessageUpdater';
import { notificationService } from '../services/notifications';
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import { queueUpdateMessage } from '../util/messageBatcher';
export type ViewSyncAttributesType = {
senderId: string;
senderE164?: string;
envelopeId: string;
removeFromMessageReceiverCache: () => unknown;
senderAci: AciString;
senderE164?: string;
senderId: string;
timestamp: number;
viewedAt: number;
};
class ViewSyncModel extends Model<ViewSyncAttributesType> {}
const viewSyncs = new Map<string, ViewSyncAttributesType>();
let singleton: ViewSyncs | undefined;
function remove(sync: ViewSyncAttributesType): void {
viewSyncs.delete(sync.envelopeId);
sync.removeFromMessageReceiverCache();
}
export class ViewSyncs extends Collection {
static getSingleton(): ViewSyncs {
if (!singleton) {
singleton = new ViewSyncs();
}
export function forMessage(
message: MessageModel
): Array<ViewSyncAttributesType> {
const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
message.attributes
)})`;
return singleton;
const sender = window.ConversationController.lookupOrCreate({
e164: message.get('source'),
serviceId: message.get('sourceServiceId'),
reason: logId,
});
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
log,
});
const viewSyncValues = Array.from(viewSyncs.values());
const matchingSyncs = viewSyncValues.filter(item => {
return item.senderId === sender?.id && item.timestamp === messageTimestamp;
});
if (matchingSyncs.length > 0) {
log.info(
`${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
);
}
matchingSyncs.forEach(sync => {
remove(sync);
});
forMessage(message: MessageModel): Array<ViewSyncModel> {
const sender = window.ConversationController.lookupOrCreate({
e164: message.get('source'),
serviceId: message.get('sourceServiceId'),
reason: 'ViewSyncs.forMessage',
});
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
log,
});
const syncs = this.filter(item => {
return (
item.get('senderId') === sender?.id &&
item.get('timestamp') === messageTimestamp
);
});
if (syncs.length) {
log.info(
`Found ${syncs.length} early view sync(s) for message ${messageTimestamp}`
);
this.remove(syncs);
}
return syncs;
}
return matchingSyncs;
}
async onSync(sync: ViewSyncModel): Promise<void> {
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.get('timestamp')
);
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
viewSyncs.set(sync.envelopeId, sync);
const found = messages.find(item => {
const sender = window.ConversationController.lookupOrCreate({
e164: item.source,
serviceId: item.sourceServiceId,
reason: 'ViewSyncs.onSync',
});
const logId = `ViewSyncs.onSync(timestamp=${sync.timestamp})`;
return sender?.id === sync.get('senderId');
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
sync.timestamp
);
const found = messages.find(item => {
const sender = window.ConversationController.lookupOrCreate({
e164: item.source,
serviceId: item.sourceServiceId,
reason: logId,
});
if (!found) {
log.info(
'Nothing found for view sync',
sync.get('senderId'),
sync.get('senderE164'),
sync.get('senderAci'),
sync.get('timestamp')
return sender?.id === sync.senderId;
});
if (!found) {
log.info(
`${logId}: nothing found`,
sync.senderId,
sync.senderE164,
sync.senderAci
);
return;
}
notificationService.removeBy({ messageId: found.id });
const message = window.MessageController.register(found.id, found);
let didChangeMessage = false;
if (message.get('readStatus') !== ReadStatus.Viewed) {
didChangeMessage = true;
message.set(markViewed(message.attributes, sync.viewedAt));
const attachments = message.get('attachments');
if (!attachments?.every(isDownloaded)) {
const updatedFields = await queueAttachmentDownloads(
message.attributes
);
return;
}
notificationService.removeBy({ messageId: found.id });
const message = window.MessageController.register(found.id, found);
let didChangeMessage = false;
if (message.get('readStatus') !== ReadStatus.Viewed) {
didChangeMessage = true;
message.set(markViewed(message.attributes, sync.get('viewedAt')));
const attachments = message.get('attachments');
if (!attachments?.every(isDownloaded)) {
const updatedFields = await queueAttachmentDownloads(
message.attributes
);
if (updatedFields) {
message.set(updatedFields);
}
if (updatedFields) {
message.set(updatedFields);
}
}
const giftBadge = message.get('giftBadge');
if (giftBadge) {
didChangeMessage = true;
message.set({
giftBadge: {
...giftBadge,
state: isIncoming(message.attributes)
? GiftBadgeStates.Redeemed
: GiftBadgeStates.Opened,
},
});
}
if (didChangeMessage) {
queueUpdateMessage(message.attributes);
}
this.remove(sync);
} catch (error) {
log.error('ViewSyncs.onSync error:', Errors.toLogFormat(error));
}
const giftBadge = message.get('giftBadge');
if (giftBadge) {
didChangeMessage = true;
message.set({
giftBadge: {
...giftBadge,
state: isIncoming(message.attributes)
? GiftBadgeStates.Redeemed
: GiftBadgeStates.Opened,
},
});
}
if (didChangeMessage) {
queueUpdateMessage(message.attributes);
}
remove(sync);
} catch (error) {
remove(sync);
log.error(`${logId} error:`, Errors.toLogFormat(error));
}
}