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/session/sending/MessageSender.ts

195 lines
5.4 KiB
TypeScript

// REMOVE COMMENT AFTER: This can just export pure functions as it doesn't need state
import { RawMessage } from '../types/RawMessage';
import { OpenGroupMessage } from '../messages/outgoing';
import { SignalService } from '../../protobuf';
import { UserUtil } from '../../util';
import { MessageEncrypter } from '../crypto';
import { encryptWithSenderKey } from '../../session/medium_group/ratchet';
import pRetry from 'p-retry';
import { PubKey } from '../types';
// ================ Regular ================
/**
* Check if we can send to service nodes.
*/
export function canSendToSnode(): boolean {
// Seems like lokiMessageAPI is not always guaranteed to be initialized
return Boolean(window.lokiMessageAPI);
}
/**
* Send a message via service nodes.
*
* @param message The message to send.
* @param attempts The amount of times to attempt sending. Minimum value is 1.
*/
export async function send(
message: RawMessage,
attempts: number = 3
): Promise<void> {
if (!canSendToSnode()) {
throw new Error('lokiMessageAPI is not initialized.');
}
const device = PubKey.cast(message.device);
const { plainTextBuffer, encryption, timestamp, ttl } = message;
const { envelopeType, cipherText } = await MessageEncrypter.encrypt(
device,
plainTextBuffer,
encryption
);
const envelope = await buildEnvelope(envelopeType, timestamp, cipherText);
const data = wrapEnvelope(envelope);
return pRetry(
async () =>
window.lokiMessageAPI.sendMessage(device.key, data, timestamp, ttl),
{
retries: Math.max(attempts - 1, 0),
factor: 1,
}
);
}
export async function sendToMediumGroup(
message: RawMessage,
groupId: string,
attempts: number = 3
): Promise<void> {
if (!canSendToSnode()) {
throw new Error('lokiMessageAPI is not initialized.');
}
const { plainTextBuffer, timestamp, ttl } = message;
const ourKey = window.textsecure.storage.user.getNumber();
const { ciphertext, keyIdx } = await encryptWithSenderKey(
plainTextBuffer,
groupId,
ourKey
);
const envelopeType = SignalService.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT;
// We should include ciphertext idx in the message
const content = SignalService.MediumGroupCiphertext.encode({
ciphertext,
source: ourKey,
keyIdx,
}).finish();
// Encrypt for the group's identity key to hide source and key idx:
const {
ciphertext: ciphertextOuter,
ephemeralKey,
} = await window.libloki.crypto.encryptForPubkey(groupId, content);
const contentOuter = SignalService.MediumGroupContent.encode({
ciphertext: ciphertextOuter,
ephemeralKey: new Uint8Array(ephemeralKey),
}).finish();
const envelope = await buildEnvelope(envelopeType, timestamp, contentOuter);
const data = wrapEnvelope(envelope);
return pRetry(
async () =>
window.lokiMessageAPI.sendMessage(groupId, data, timestamp, ttl),
{
retries: Math.max(attempts - 1, 0),
factor: 1,
}
);
}
async function buildEnvelope(
type: SignalService.Envelope.Type,
timestamp: number,
content: Uint8Array
): Promise<SignalService.Envelope> {
let source: string | undefined;
if (type !== SignalService.Envelope.Type.UNIDENTIFIED_SENDER) {
source = await UserUtil.getCurrentDevicePubKey();
}
return SignalService.Envelope.create({
type,
source,
sourceDevice: 1,
timestamp,
content,
});
}
/**
* This is an outdated practice and we should probably just send the envelope data directly.
* Something to think about in the future.
*/
function wrapEnvelope(envelope: SignalService.Envelope): Uint8Array {
const request = SignalService.WebSocketRequestMessage.create({
id: 0,
body: SignalService.Envelope.encode(envelope).finish(),
verb: 'PUT',
path: '/api/v1/message',
});
const websocket = SignalService.WebSocketMessage.create({
type: SignalService.WebSocketMessage.Type.REQUEST,
request,
});
return SignalService.WebSocketMessage.encode(websocket).finish();
}
// ================ Open Group ================
/**
* Send a message to an open group.
* @param message The open group message.
*/
export async function sendToOpenGroup(
message: OpenGroupMessage
): Promise<boolean> {
/*
Note: Retrying wasn't added to this but it can be added in the future if needed.
The only problem is that `channelAPI.sendMessage` returns true/false and doesn't throw any error so we can never be sure why sending failed.
This should be fixed and we shouldn't rely on returning true/false, rather return nothing (success) or throw an error (failure)
*/
const { group, quote, attachments, preview, body, timestamp } = message;
const channelAPI = await window.lokiPublicChatAPI.findOrCreateChannel(
group.server,
group.channel,
group.conversationId
);
if (!channelAPI) {
return false;
}
// Don't think returning true/false on `sendMessage` is a good way
return channelAPI.sendMessage(
{
quote,
attachments: attachments || [],
preview: preview || [],
body,
},
timestamp
);
// TODO: The below should be handled in whichever class calls this
/*
const res = await sendToOpenGroup(message);
if (!res) {
throw new textsecure.PublicChatError('Failed to send public chat message');
}
const messageEventData = {
pubKey,
timestamp: messageTimeStamp,
};
messageEventData.serverId = res;
window.Whisper.events.trigger('publicMessageSent', messageEventData);
*/
}