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

239 lines
7.3 KiB
TypeScript

import _ from 'lodash';
import { ClosedGroup, getMessageQueue } from '..';
import { ConversationTypeEnum } from '../../models/types';
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 { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { generateClosedGroupPublicKey, generateCurve25519KeyPairWithoutPrefix } from '../crypto';
import {
ClosedGroupNewMessage,
ClosedGroupNewMessageParams,
} from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { PubKey } from '../types';
import { UserUtils } from '../utils';
import { forceSyncConfigurationNowIfNeeded } from '../utils/sync/syncUtils';
import { getConversationController } from './ConversationController';
export async function createClosedGroup(groupName: string, members: Array<string>, isV3: boolean) {
const setOfMembers = new Set(members);
if (isV3) {
throw new Error('groupv3 is not supported yet');
}
const us = UserUtils.getOurPubKeyStrFromCache();
const groupPublicKey = await generateClosedGroupPublicKey();
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,
ConversationTypeEnum.GROUP
);
convo.set('lastJoinedTimestamp', Date.now());
await convo.setIsApproved(true, false);
// Ensure the current user is a member
setOfMembers.add(us);
const listOfMembers = [...setOfMembers];
const admins = [us];
const existingExpirationType = 'unknown';
const existingExpireTimer = 0;
const groupDetails: ClosedGroup.GroupInfo = {
id: groupPublicKey,
name: groupName,
members: listOfMembers,
admins,
activeAt: Date.now(),
// TODO This is only applicable for old closed groups - will be removed in future
expirationType: existingExpirationType,
expireTimer: existingExpireTimer,
};
// we don't want the initial "AAA and You joined the group" anymore
// 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(groupDetails);
await convo.commit();
convo.updateLastMessage();
// Send a closed group update message to all members individually.
// Note: we do not make those messages expire
const allInvitesSent = await sendToGroupMembers(
listOfMembers,
groupPublicKey,
groupName,
admins,
encryptionKeyPair
);
if (allInvitesSent) {
const newHexKeypair = encryptionKeyPair.toHexKeyPair();
await addKeyPairToCacheAndDBIfNeeded(groupPublicKey, newHexKeypair);
// Subscribe to this group id
getSwarmPollingInstance().addGroupId(new PubKey(groupPublicKey));
}
// commit again as now the keypair is saved and can be added to the libsession wrapper UserGroup
await convo.commit();
await forceSyncConfigurationNowIfNeeded();
await openConversationWithMessages({ conversationKey: groupPublicKey, messageId: null });
}
function getMessageArgs(group_name: string, names: Array<string>) {
const name = names[0];
switch (names.length) {
case 1:
return {
token: 'groupInviteFailedUser',
args: {
group_name,
name,
},
} as const;
case 2:
return {
token: 'groupInviteFailedTwo',
args: {
group_name,
name,
other_name: names[1],
},
} as const;
default:
return {
token: 'groupInviteFailedMultiple',
args: {
group_name,
name,
count: names.length - 1,
},
} as const;
}
}
/**
* Sends a group invite message to each member of the group.
* @returns Array of promises for group invite messages sent to group members.
*/
async function sendToGroupMembers(
listOfMembers: Array<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair,
isRetry: boolean = false
): Promise<any> {
const promises = createInvitePromises(
listOfMembers,
groupPublicKey,
groupName,
admins,
encryptionKeyPair
);
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 (isRetry) {
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('groupInviteSuccessful'),
i18nMessage: { token: 'groupInviteSuccessful' },
hideCancel: true,
onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
},
})
);
}
return allInvitesSent;
}
// Confirmation dialog that recursively calls sendToGroupMembers on resolve
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);
}
});
const namesOfMembersToResend = membersToResend.map(
m =>
getConversationController().get(m)?.getNicknameOrRealUsernameOrPlaceholder() ||
window.i18n('unknown')
);
if (membersToResend.length < 1) {
throw new Error('Some invites failed, we should have found members to resend');
}
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('groupError'),
i18nMessage: getMessageArgs(groupName, namesOfMembersToResend),
okText: window.i18n('resend'),
onClickOk: async () => {
if (membersToResend.length > 0) {
const isRetrySend = true;
await sendToGroupMembers(
membersToResend,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
isRetrySend
);
}
},
onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
},
})
);
return allInvitesSent;
}
function createInvitePromises(
listOfMembers: Array<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair
) {
return listOfMembers.map(async m => {
const messageParams: ClosedGroupNewMessageParams = {
groupId: groupPublicKey,
name: groupName,
members: listOfMembers,
admins,
keypair: encryptionKeyPair,
timestamp: Date.now(),
expirationType: null, // we keep that one **not** expiring
expireTimer: 0,
};
const message = new ClosedGroupNewMessage(messageParams);
return getMessageQueue().sendToPubKeyNonDurably({
pubkey: PubKey.cast(m),
message,
namespace: SnodeNamespaces.UserMessages,
});
});
}