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, 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) { 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, groupPublicKey: string, groupName: string, admins: Array, encryptionKeyPair: ECKeyPair, isRetry: boolean = false ): Promise { 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 = new Array(); 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, groupPublicKey: string, groupName: string, admins: Array, 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, }); }); }