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.
239 lines
7.3 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
}
|