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/conversations/createClosedGroup.ts

236 lines
7.7 KiB
TypeScript

import _ from 'lodash';
import { ClosedGroup, getMessageQueue } from '..';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { MessageModel } from '../../models/message';
import { addKeyPairToCacheAndDBIfNeeded } from '../../receiver/closedGroups';
import { ECKeyPair } from '../../receiver/keypairs';
import { openConversationWithMessages } from '../../state/ducks/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { getSwarmPollingInstance } from '../apis/snode_api';
import {
generateClosedGroupPublicKey,
generateCurve25519KeyPairWithoutPrefix,
generateGroupV3Keypair,
} from '../crypto';
import {
ClosedGroupNewMessage,
ClosedGroupNewMessageParams,
} from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { PubKey } from '../types';
import { UserUtils } from '../utils';
import { forceSyncConfigurationNowIfNeeded } from '../utils/syncUtils';
import { getConversationController } from './ConversationController';
export async function createClosedGroup(groupName: string, members: Array<string>, isV3: boolean) {
const setOfMembers = new Set(members);
const us = UserUtils.getOurPubKeyStrFromCache();
const identityKeyPair = await generateGroupV3Keypair();
if (!identityKeyPair) {
throw new Error('Could not create identity keypair for new closed group v3');
}
// a v3 pubkey starts with 03 and an old one starts with 05
const groupPublicKey = isV3 ? identityKeyPair.pubkey : await generateClosedGroupPublicKey();
// the first encryption keypair is generated the same for all versions of closed group
const encryptionKeyPair = await generateCurve25519KeyPairWithoutPrefix();
if (!encryptionKeyPair) {
throw new Error('Could not create encryption keypair for new closed group');
}
// Create the group
const convo = await getConversationController().getOrCreateAndWait(
groupPublicKey,
isV3 ? ConversationTypeEnum.GROUPV3 : ConversationTypeEnum.GROUP
);
await convo.setIsApproved(true, false);
// Ensure the current user is a member
setOfMembers.add(us);
const listOfMembers = [...setOfMembers];
const admins = [us];
const existingExpireTimer = 0;
const groupDetails: ClosedGroup.GroupInfo = {
id: groupPublicKey,
name: groupName,
members: listOfMembers,
admins,
activeAt: Date.now(),
expireTimer: existingExpireTimer,
};
const groupDetailsV3: ClosedGroup.GroupInfoV3 = {
id: groupPublicKey,
identityPrivateKey: isV3 ? identityKeyPair.privateKey : undefined,
isV3: true,
name: groupName,
members: listOfMembers,
admins,
activeAt: Date.now(),
expireTimer: existingExpireTimer,
};
// used for UI only, adding of a message to remind who is in the group and the name of the group
const groupDiff: ClosedGroup.GroupDiff = {
newName: groupName,
joiningMembers: listOfMembers,
};
const dbMessage = await ClosedGroup.addUpdateMessage(convo, groupDiff, us, Date.now());
// be sure to call this before sending the message.
// the sending pipeline needs to know from GroupUtils when a message is for a medium group
await ClosedGroup.updateOrCreateClosedGroup(isV3 ? groupDetailsV3 : groupDetails);
await convo.commit();
convo.updateLastMessage();
if (isV3) {
// we need to send a group info and encryption keys message to the batch endpoint with both seqno being 0
throw new Error('fixme');
}
// Send a closed group update message to all members individually
const allInvitesSent = await sendToGroupMembers(
listOfMembers,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
dbMessage,
existingExpireTimer
);
if (allInvitesSent) {
const newHexKeypair = encryptionKeyPair.toHexKeyPair();
const isHexKeyPairSaved = await addKeyPairToCacheAndDBIfNeeded(groupPublicKey, newHexKeypair);
if (isHexKeyPairSaved) {
window?.log?.info('Dropping already saved keypair for group', groupPublicKey);
}
// Subscribe to this group id
getSwarmPollingInstance().addGroupId(new PubKey(groupPublicKey));
}
await forceSyncConfigurationNowIfNeeded();
await openConversationWithMessages({ conversationKey: groupPublicKey, messageId: null });
}
/**
* Sends a group invite message to each member of the group.
* @returns Array of promises for group invite messages sent to group members.
* This function takes care of the groupv3 specificities
*/
async function sendToGroupMembers(
listOfMembers: Array<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair,
dbMessage: MessageModel,
existingExpireTimer: number,
isRetry: boolean = false
): Promise<any> {
const promises = createInvitePromises(
listOfMembers,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
dbMessage,
existingExpireTimer
);
window?.log?.info(`Sending invites for group ${groupPublicKey} to ${listOfMembers}`);
// evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
const inviteResults = await Promise.all(promises);
const allInvitesSent = _.every(inviteResults, inviteResult => inviteResult !== false);
if (allInvitesSent) {
// if (true) {
if (isRetry) {
const invitesTitle =
inviteResults.length > 1
? window.i18n('closedGroupInviteSuccessTitlePlural')
: window.i18n('closedGroupInviteSuccessTitle');
window.inboxStore?.dispatch(
updateConfirmModal({
title: invitesTitle,
message: window.i18n('closedGroupInviteSuccessMessage'),
hideCancel: true,
})
);
}
return allInvitesSent;
} else {
// Confirmation dialog that recursively calls sendToGroupMembers on resolve
window.inboxStore?.dispatch(
updateConfirmModal({
title:
inviteResults.length > 1
? window.i18n('closedGroupInviteFailTitlePlural')
: window.i18n('closedGroupInviteFailTitle'),
message:
inviteResults.length > 1
? window.i18n('closedGroupInviteFailMessagePlural')
: window.i18n('closedGroupInviteFailMessage'),
okText: window.i18n('closedGroupInviteOkText'),
onClickOk: async () => {
const membersToResend: Array<string> = new Array<string>();
inviteResults.forEach((result, index) => {
const member = listOfMembers[index];
// group invite must always contain the admin member.
if (result !== true || admins.includes(member)) {
membersToResend.push(member);
}
});
if (membersToResend.length > 0) {
const isRetrySend = true;
await sendToGroupMembers(
membersToResend,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
dbMessage,
existingExpireTimer,
isRetrySend
);
}
},
})
);
}
return allInvitesSent;
}
function createInvitePromises(
listOfMembers: Array<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair,
dbMessage: MessageModel,
existingExpireTimer: number
) {
return listOfMembers.map(async m => {
const messageParams: ClosedGroupNewMessageParams = {
groupId: groupPublicKey,
name: groupName,
members: listOfMembers,
admins,
keypair: encryptionKeyPair,
timestamp: Date.now(),
identifier: dbMessage.id,
expireTimer: existingExpireTimer,
};
const message = new ClosedGroupNewMessage(messageParams);
return getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(m), message);
});
}