You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/receiver/dataMessage.ts

353 lines
10 KiB
TypeScript

import { SignalService } from './../protobuf';
import { removeFromCache } from './cache';
import { EnvelopePlus } from './types';
import { getEnvelopeId } from './common';
import { PubKey } from '../session/types';
import { handleMessageJob, toRegularMessage } from './queuedJob';
import { isEmpty, isFinite, noop, omit, toNumber } from 'lodash';
import { StringUtils, UserUtils } from '../session/utils';
import { getConversationController } from '../session/conversations';
import { handleClosedGroupControlMessage } from './closedGroups';
import { Data } from '../../ts/data/data';
import { ConversationModel } from '../models/conversation';
import {
createSwarmMessageSentFromNotUs,
createSwarmMessageSentFromUs,
} from '../models/messageFactory';
import { MessageModel } from '../models/message';
import { isUsFromCache } from '../session/utils/User';
import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates';
import { toLogFormat } from '../types/attachments/Errors';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { handleMessageReaction } from '../util/reactions';
function cleanAttachment(attachment: any) {
return {
...omit(attachment, 'thumbnail'),
id: attachment.id.toString(),
key: attachment.key ? StringUtils.decode(attachment.key, 'base64') : null,
digest:
attachment.digest && attachment.digest.length > 0
? StringUtils.decode(attachment.digest, 'base64')
: null,
};
}
function cleanAttachments(decrypted: SignalService.DataMessage) {
const { quote, group } = decrypted;
// Here we go from binary to string/base64 in all AttachmentPointer digest/key fields
// we do not care about group.avatar on Session
if (group && group.avatar !== null) {
group.avatar = null;
}
decrypted.attachments = (decrypted.attachments || []).map(cleanAttachment);
decrypted.preview = (decrypted.preview || []).map((item: any) => {
const { image } = item;
if (!image) {
return item;
}
return {
...item,
image: cleanAttachment(image),
};
});
if (quote) {
if (quote.id) {
quote.id = toNumber(quote.id);
}
quote.attachments = (quote.attachments || []).map((item: any) => {
const { thumbnail } = item;
if (!thumbnail || thumbnail.length === 0) {
return item;
}
return {
...item,
thumbnail: cleanAttachment(item.thumbnail),
};
});
}
}
export function isMessageEmpty(message: SignalService.DataMessage) {
const {
flags,
body,
attachments,
group,
quote,
preview,
openGroupInvitation,
reaction,
} = message;
return (
!flags &&
// FIXME remove this hack to drop auto friend requests messages in a few weeks 15/07/2020
isBodyEmpty(body) &&
isEmpty(attachments) &&
isEmpty(group) &&
isEmpty(quote) &&
isEmpty(preview) &&
isEmpty(openGroupInvitation) &&
isEmpty(reaction)
);
}
function isBodyEmpty(body: string) {
return isEmpty(body);
}
export function cleanIncomingDataMessage(
rawDataMessage: SignalService.DataMessage,
envelope?: EnvelopePlus
) {
/* tslint:disable:no-bitwise */
const FLAGS = SignalService.DataMessage.Flags;
// Now that its decrypted, validate the message and clean it up for consumer
// processing
// Note that messages may (generally) only perform one action and we ignore remaining
// fields after the first action.
if (rawDataMessage.flags == null) {
rawDataMessage.flags = 0;
}
if (rawDataMessage.expireTimer == null) {
rawDataMessage.expireTimer = 0;
}
if (rawDataMessage.flags & FLAGS.EXPIRATION_TIMER_UPDATE) {
rawDataMessage.body = '';
rawDataMessage.attachments = [];
} else if (rawDataMessage.flags !== 0) {
throw new Error('Unknown flags in message');
}
const attachmentCount = rawDataMessage?.attachments?.length || 0;
const ATTACHMENT_MAX = 32;
if (attachmentCount > ATTACHMENT_MAX) {
throw new Error(
`Too many attachments: ${attachmentCount} included in one message, max is ${ATTACHMENT_MAX}`
);
}
cleanAttachments(rawDataMessage);
// if the decrypted dataMessage timestamp is not set, copy the one from the envelope
if (!isFinite(rawDataMessage?.timestamp) && envelope) {
rawDataMessage.timestamp = envelope.timestamp;
}
return rawDataMessage;
}
/**
* We have a few origins possible
* - if the message is from a private conversation with a friend and he wrote to us,
* the conversation to add the message to is our friend pubkey, so envelope.source
* - if the message is from a medium group conversation
* * envelope.source is the medium group pubkey
* * envelope.senderIdentity is the author pubkey (the one who sent the message)
* - at last, if the message is a syncMessage,
* * envelope.source is our pubkey (our other device has the same pubkey as us)
* * dataMessage.syncTarget is either the group public key OR the private conversation this message is about.
*/
// tslint:disable-next-line: cyclomatic-complexity
export async function handleSwarmDataMessage(
envelope: EnvelopePlus,
sentAtTimestamp: number,
rawDataMessage: SignalService.DataMessage,
messageHash: string,
senderConversationModel: ConversationModel
): Promise<void> {
window.log.info('handleSwarmDataMessage');
const cleanDataMessage = cleanIncomingDataMessage(rawDataMessage, envelope);
// we handle group updates from our other devices in handleClosedGroupControlMessage()
if (cleanDataMessage.closedGroupControlMessage) {
await handleClosedGroupControlMessage(
envelope,
cleanDataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage
);
return;
}
/**
* This is a mess, but
*
* 1. if syncTarget is set and this is a synced message, syncTarget holds the conversationId in which this message is addressed. This syncTarget can be a private conversation pubkey or a closed group pubkey
*
* 2. for a closed group message, envelope.senderIdentity is the pubkey of the sender and envelope.source is the pubkey of the closed group.
*
* 3. for a private conversation message, envelope.senderIdentity and envelope.source are probably the pubkey of the sender.
*/
const isSyncedMessage = Boolean(cleanDataMessage.syncTarget?.length);
// no need to remove prefix here, as senderIdentity set => envelope.source is not used (and this is the one having the prefix when this is an opengroup)
const convoIdOfSender = envelope.senderIdentity || envelope.source;
const isMe = UserUtils.isUsFromCache(convoIdOfSender);
if (isSyncedMessage && !isMe) {
window?.log?.warn('Got a sync message from someone else than me. Dropping it.');
return removeFromCache(envelope);
} else if (isSyncedMessage) {
// we should create the synTarget convo but I have no idea how to know if this is a private or closed group convo?
}
const convoIdToAddTheMessageTo = PubKey.removeTextSecurePrefixIfNeeded(
isSyncedMessage ? cleanDataMessage.syncTarget : envelope.source
);
const convoToAddMessageTo = await getConversationController().getOrCreateAndWait(
convoIdToAddTheMessageTo,
envelope.senderIdentity ? ConversationTypeEnum.GROUP : ConversationTypeEnum.PRIVATE
);
window?.log?.info(
`Handle dataMessage about convo ${convoIdToAddTheMessageTo} from user: ${convoIdOfSender}`
);
// remove the prefix from the source object so this is correct for all other
// Check if we need to update any profile names
if (
!isMe &&
senderConversationModel &&
cleanDataMessage.profile &&
cleanDataMessage.profileKey?.length
) {
// do not await this
void appendFetchAvatarAndProfileJob(
senderConversationModel,
cleanDataMessage.profile,
cleanDataMessage.profileKey
);
}
if (isMessageEmpty(cleanDataMessage)) {
window?.log?.warn(`Message ${getEnvelopeId(envelope)} ignored; it was empty`);
return removeFromCache(envelope);
}
if (!convoIdToAddTheMessageTo) {
window?.log?.error('We cannot handle a message without a conversationId');
confirm();
return;
}
const msgModel =
isSyncedMessage || (envelope.senderIdentity && isUsFromCache(envelope.senderIdentity))
? createSwarmMessageSentFromUs({
conversationId: convoIdToAddTheMessageTo,
messageHash,
sentAt: sentAtTimestamp,
})
: createSwarmMessageSentFromNotUs({
conversationId: convoIdToAddTheMessageTo,
messageHash,
sender: senderConversationModel.id,
sentAt: sentAtTimestamp,
});
await handleSwarmMessage(
msgModel,
messageHash,
sentAtTimestamp,
cleanDataMessage,
convoToAddMessageTo,
() => removeFromCache(envelope)
);
}
export async function isSwarmMessageDuplicate({
source,
sentAt,
}: {
source: string;
sentAt: number;
}) {
try {
const result = await Data.getMessageBySenderAndSentAt({
source,
sentAt,
});
return Boolean(result);
} catch (error) {
window?.log?.error('isSwarmMessageDuplicate error:', toLogFormat(error));
return false;
}
}
export async function handleOutboxMessageModel(
msgModel: MessageModel,
messageHash: string,
sentAt: number,
rawDataMessage: SignalService.DataMessage,
convoToAddMessageTo: ConversationModel
) {
return handleSwarmMessage(
msgModel,
messageHash,
sentAt,
rawDataMessage,
convoToAddMessageTo,
noop
);
}
async function handleSwarmMessage(
msgModel: MessageModel,
messageHash: string,
sentAt: number,
rawDataMessage: SignalService.DataMessage,
convoToAddMessageTo: ConversationModel,
confirm: () => void
): Promise<void> {
if (!rawDataMessage || !msgModel) {
window?.log?.warn('Invalid data passed to handleSwarmMessage.');
confirm();
return;
}
void convoToAddMessageTo.queueJob(async () => {
// this call has to be made inside the queueJob!
if (!msgModel.get('isPublic') && rawDataMessage.reaction && rawDataMessage.syncTarget) {
await handleMessageReaction(
rawDataMessage.reaction,
msgModel.get('source'),
false,
messageHash
);
confirm();
return;
}
const isDuplicate = await isSwarmMessageDuplicate({
source: msgModel.get('source'),
sentAt,
});
if (isDuplicate) {
window?.log?.info('Received duplicate message. Dropping it.');
confirm();
return;
}
await handleMessageJob(
msgModel,
convoToAddMessageTo,
toRegularMessage(rawDataMessage),
confirm,
msgModel.get('source'),
messageHash
);
});
}