mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
/* eslint-disable no-param-reassign */
|
|
import { isEmpty, omit } from 'lodash';
|
|
|
|
import { SignalService } from '../protobuf';
|
|
import { getEnvelopeId } from './common';
|
|
import { type BaseDecodedEnvelope } from './types';
|
|
|
|
import { Data } from '../data/data';
|
|
import { ConversationModel } from '../models/conversation';
|
|
import { ConvoHub } from '../session/conversations';
|
|
import { PubKey } from '../session/types';
|
|
import { StringUtils, UserUtils } from '../session/utils';
|
|
import { handleMessageJob, toRegularMessage } from './queuedJob';
|
|
|
|
import { MessageModel } from '../models/message';
|
|
import {
|
|
createSwarmMessageSentFromNotUs,
|
|
createSwarmMessageSentFromUs,
|
|
} from '../models/messageFactory';
|
|
import { DisappearingMessages } from '../session/disappearing_messages';
|
|
import { WithDisappearingMessageUpdate } from '../session/disappearing_messages/types';
|
|
import { ProfileManager } from '../session/profile_manager/ProfileManager';
|
|
import { isUsFromCache } from '../session/utils/User';
|
|
import { Action, Reaction } from '../types/Reaction';
|
|
import { toLogFormat } from '../types/attachments/Errors';
|
|
import { Reactions } from '../util/reactions';
|
|
import { GroupV2Receiver } from './groupv2/handleGroupV2Message';
|
|
import { ConversationTypeEnum } from '../models/types';
|
|
import { ed25519Str } from '../session/utils/String';
|
|
import { longOrNumberToNumber } from '../types/long/longOrNumberToNumber';
|
|
import { buildPrivateProfileChangeFromSwarmDataMessage } from '../models/profile';
|
|
|
|
function cleanAttachment(attachment: SignalService.IAttachmentPointer) {
|
|
return {
|
|
...omit(attachment, 'thumbnail'),
|
|
id: 0,
|
|
key: attachment.key ? StringUtils.decode(attachment.key, 'base64') : null,
|
|
digest:
|
|
attachment.digest && attachment.digest.length > 0
|
|
? StringUtils.decode(attachment.digest, 'base64')
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function cleanAttachments(decryptedDataMessage: SignalService.DataMessage) {
|
|
const { quote } = decryptedDataMessage;
|
|
|
|
// Here we go from binary to string/base64 in all AttachmentPointer digest/key fields
|
|
|
|
// when receiving a message we get keys of attachment as buffer, but we override the data with the decrypted string instead.
|
|
// TODO it would be nice to get rid of that as any here, but not in this PR
|
|
decryptedDataMessage.attachments = (decryptedDataMessage.attachments || []).map(
|
|
cleanAttachment
|
|
) as any;
|
|
decryptedDataMessage.preview = (decryptedDataMessage.preview || []).map((item: any) => {
|
|
const { image } = item;
|
|
|
|
if (!image) {
|
|
return item;
|
|
}
|
|
|
|
return {
|
|
...item,
|
|
image: cleanAttachment(image),
|
|
};
|
|
});
|
|
|
|
if (quote) {
|
|
if (quote.id) {
|
|
quote.id = longOrNumberToNumber(quote.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function messageHasVisibleContent(message: SignalService.DataMessage) {
|
|
const { flags, body, attachments, quote, preview, openGroupInvitation, reaction } = message;
|
|
|
|
return (
|
|
!!flags ||
|
|
!isEmpty(body) ||
|
|
!isEmpty(attachments) ||
|
|
!isEmpty(quote) ||
|
|
!isEmpty(preview) ||
|
|
!isEmpty(openGroupInvitation) ||
|
|
!isEmpty(reaction)
|
|
);
|
|
}
|
|
|
|
export function cleanIncomingDataMessage(rawDataMessage: SignalService.DataMessage) {
|
|
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;
|
|
}
|
|
// eslint-disable-next-line no-bitwise
|
|
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);
|
|
|
|
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.
|
|
*/
|
|
export async function handleSwarmDataMessage({
|
|
decodedEnvelope,
|
|
rawDataMessage,
|
|
senderConversationModel,
|
|
expireUpdate,
|
|
}: WithDisappearingMessageUpdate & {
|
|
decodedEnvelope: BaseDecodedEnvelope;
|
|
rawDataMessage: SignalService.DataMessage;
|
|
senderConversationModel: ConversationModel;
|
|
}): Promise<void> {
|
|
window.log.info('handleSwarmDataMessage');
|
|
|
|
const cleanDataMessage = cleanIncomingDataMessage(rawDataMessage);
|
|
|
|
if (cleanDataMessage.groupUpdateMessage) {
|
|
await GroupV2Receiver.handleGroupUpdateMessage({
|
|
signatureTimestamp: decodedEnvelope.sentAtMs,
|
|
updateMessage: rawDataMessage.groupUpdateMessage as SignalService.GroupUpdateMessage,
|
|
source: decodedEnvelope.source,
|
|
senderIdentity: decodedEnvelope.senderIdentity,
|
|
expireUpdate,
|
|
messageHash: decodedEnvelope.messageHash,
|
|
});
|
|
// Groups update should always be able to be decrypted as we get the keys before trying to decrypt them.
|
|
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 = decodedEnvelope.getAuthor();
|
|
const isMe = UserUtils.isUsFromCache(convoIdOfSender);
|
|
|
|
if (isSyncedMessage && !isMe) {
|
|
window?.log?.warn('Got a sync message from someone else than me. Dropping it.');
|
|
return;
|
|
}
|
|
const convoIdToAddTheMessageTo = isSyncedMessage
|
|
? cleanDataMessage.syncTarget
|
|
: decodedEnvelope.source;
|
|
if (convoIdToAddTheMessageTo.startsWith(PubKey.PREFIX_GROUP_TEXTSECURE)) {
|
|
window?.log?.warn(
|
|
'got a message starting with textsecure prefix. can only be legacy group message. dropping.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const isGroupMessage = !!decodedEnvelope.senderIdentity;
|
|
const isGroupV2Message = isGroupMessage && PubKey.is03Pubkey(decodedEnvelope.source);
|
|
let typeOfConvo = ConversationTypeEnum.PRIVATE;
|
|
if (isGroupV2Message) {
|
|
typeOfConvo = ConversationTypeEnum.GROUPV2;
|
|
} else if (isGroupMessage) {
|
|
typeOfConvo = ConversationTypeEnum.GROUP;
|
|
}
|
|
|
|
window?.log?.info(
|
|
`Handle dataMessage about convo ${ed25519Str(convoIdToAddTheMessageTo)} from user: ${ed25519Str(convoIdOfSender)}`
|
|
);
|
|
|
|
// remove the prefix from the source object so this is correct for all other
|
|
const convoToAddMessageTo = await ConvoHub.use().getOrCreateAndWait(
|
|
convoIdToAddTheMessageTo,
|
|
typeOfConvo
|
|
);
|
|
|
|
// Check if we need to update any profile names
|
|
if (!isMe && senderConversationModel) {
|
|
const profile = buildPrivateProfileChangeFromSwarmDataMessage({
|
|
convo: senderConversationModel,
|
|
dataMessage: cleanDataMessage,
|
|
decodedPro: decodedEnvelope.validPro,
|
|
});
|
|
if (profile) {
|
|
await ProfileManager.updateProfileOfContact(profile);
|
|
}
|
|
}
|
|
|
|
if (!messageHasVisibleContent(cleanDataMessage)) {
|
|
window?.log?.warn(`Message ${getEnvelopeId(decodedEnvelope)} ignored; it was empty`);
|
|
}
|
|
|
|
if (!convoIdToAddTheMessageTo) {
|
|
window?.log?.error('We cannot handle a message without a conversationId');
|
|
return;
|
|
}
|
|
|
|
let msgModel =
|
|
isSyncedMessage || isUsFromCache(decodedEnvelope.getAuthor())
|
|
? createSwarmMessageSentFromUs({
|
|
conversationId: convoIdToAddTheMessageTo,
|
|
messageHash: decodedEnvelope.messageHash,
|
|
sentAt: decodedEnvelope.sentAtMs,
|
|
})
|
|
: createSwarmMessageSentFromNotUs({
|
|
conversationId: convoIdToAddTheMessageTo,
|
|
messageHash: decodedEnvelope.messageHash,
|
|
sender: senderConversationModel.id,
|
|
sentAt: decodedEnvelope.sentAtMs,
|
|
});
|
|
|
|
if (!isEmpty(expireUpdate)) {
|
|
msgModel = DisappearingMessages.getMessageReadyToDisappear(
|
|
convoToAddMessageTo,
|
|
msgModel,
|
|
cleanDataMessage.flags,
|
|
expireUpdate
|
|
);
|
|
}
|
|
|
|
await handleSwarmMessage(msgModel, cleanDataMessage, convoToAddMessageTo, decodedEnvelope);
|
|
}
|
|
|
|
export async function isSwarmMessageDuplicate({
|
|
source,
|
|
sentAt,
|
|
}: {
|
|
source: string;
|
|
sentAt: number;
|
|
}) {
|
|
try {
|
|
const result = (
|
|
await Data.getMessagesBySenderAndSentAt([
|
|
{
|
|
source,
|
|
timestamp: sentAt,
|
|
},
|
|
])
|
|
)?.length;
|
|
|
|
return Boolean(result);
|
|
} catch (error) {
|
|
window?.log?.error('isSwarmMessageDuplicate error:', toLogFormat(error));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function handleOutboxMessageModel(
|
|
msgModel: MessageModel,
|
|
rawDataMessage: SignalService.DataMessage,
|
|
convoToAddMessageTo: ConversationModel,
|
|
decodedEnvelope: BaseDecodedEnvelope
|
|
) {
|
|
return handleSwarmMessage(msgModel, rawDataMessage, convoToAddMessageTo, decodedEnvelope);
|
|
}
|
|
|
|
async function handleSwarmMessage(
|
|
msgModel: MessageModel,
|
|
rawDataMessage: SignalService.DataMessage,
|
|
convoToAddMessageTo: ConversationModel,
|
|
decodedEnvelope: BaseDecodedEnvelope
|
|
): Promise<void> {
|
|
if (!rawDataMessage || !msgModel) {
|
|
window?.log?.warn('Invalid data passed to handleSwarmMessage.');
|
|
return;
|
|
}
|
|
|
|
void convoToAddMessageTo.queueJob(async () => {
|
|
// this call has to be made inside the queueJob!
|
|
// We handle reaction DataMessages separately
|
|
if (!convoToAddMessageTo.isOpenGroupV2() && rawDataMessage.reaction) {
|
|
await Reactions.handleMessageReaction({
|
|
reaction: rawDataMessage.reaction,
|
|
sender: msgModel.get('source'),
|
|
you: isUsFromCache(msgModel.get('source')),
|
|
});
|
|
|
|
if (
|
|
convoToAddMessageTo.isPrivate() &&
|
|
msgModel.get('unread') &&
|
|
rawDataMessage.reaction.action === Action.REACT
|
|
) {
|
|
msgModel.setKey('reaction', rawDataMessage.reaction as Reaction);
|
|
convoToAddMessageTo.throttledNotify(msgModel);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const isDuplicate = await isSwarmMessageDuplicate({
|
|
source: msgModel.get('source'),
|
|
sentAt: decodedEnvelope.sentAtMs,
|
|
});
|
|
|
|
if (isDuplicate) {
|
|
window?.log?.info('Received duplicate message. Dropping it.');
|
|
return;
|
|
}
|
|
|
|
await handleMessageJob(
|
|
msgModel,
|
|
convoToAddMessageTo,
|
|
toRegularMessage(rawDataMessage),
|
|
decodedEnvelope
|
|
);
|
|
});
|
|
}
|