feat: add building and sending of invite messages

pull/2963/head
Audric Ackermann 2 years ago
parent 6ed74c9807
commit d7608c42b6

@ -0,0 +1,51 @@
import { GroupMemberGet, GroupPubkeyType, Uint8ArrayLen64 } from 'libsession_util_nodejs';
import { compact } from 'lodash';
import { MetaGroupWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { GetNetworkTime } from '../../apis/snode_api/getNetworkTime';
import { GroupUpdateInviteMessage } from '../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage';
import { UserUtils } from '../../utils';
import { getSodiumRenderer } from '../MessageEncrypter';
export async function getGroupInvitesMessages({
groupName,
membersFromWrapper,
secretKey,
groupPk,
}: {
membersFromWrapper: Array<GroupMemberGet>;
groupName: string;
secretKey: Uint8ArrayLen64; // len 64
groupPk: GroupPubkeyType;
}) {
const sodium = await getSodiumRenderer();
const timestamp = GetNetworkTime.getNowWithNetworkOffset();
const inviteDetails = compact(
await Promise.all(
membersFromWrapper.map(async ({ pubkeyHex: member }) => {
if (UserUtils.isUsFromCache(member)) {
return null;
}
const tosign = `INVITE${member}${timestamp}`;
// Note: as the signature is built with the timestamp here, we cannot override the timestamp later on the sending pipeline
const adminSignature = sodium.crypto_sign_detached(tosign, secretKey);
console.info(`before makeSwarmSubAccount ${groupPk}:${member}`);
const memberAuthData = await MetaGroupWrapperActions.makeSwarmSubAccount(groupPk, member);
debugger;
console.info(`after makeSwarmSubAccount ${groupPk}:${member}`);
const invite = new GroupUpdateInviteMessage({
groupName,
groupPk,
timestamp,
adminSignature,
memberAuthData,
});
return { member, invite };
})
)
);
return inviteDetails;
}

@ -1,6 +1,7 @@
import { PubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { Preconditions } from '../../../preconditions';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
type Params = GroupUpdateMessageParams & {
@ -23,6 +24,18 @@ export class GroupUpdateDeleteMemberContentMessage extends GroupUpdateMessage {
if (isEmpty(this.memberSessionIds)) {
throw new Error('GroupUpdateDeleteMemberContentMessage needs members in list');
}
Preconditions.checkUin8tArrayOrThrow({
data: this.adminSignature,
expectedLength: 64,
varName: 'adminSignature',
context: this.constructor.toString(),
});
Preconditions.checkArrayHaveOnly05Pubkeys({
arr: this.memberSessionIds,
context: this.constructor.toString(),
varName: 'memberSessionIds',
});
}
public dataProto(): SignalService.DataMessage {

@ -1,4 +1,5 @@
import { SignalService } from '../../../../../../protobuf';
import { Preconditions } from '../../../preconditions';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
interface Params extends GroupUpdateMessageParams {
@ -15,6 +16,13 @@ export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
super(params);
this.adminSignature = params.adminSignature;
Preconditions.checkUin8tArrayOrThrow({
data: this.adminSignature,
expectedLength: 64,
varName: 'adminSignature',
context: this.constructor.toString(),
});
}
public dataProto(): SignalService.DataMessage {

@ -25,18 +25,19 @@ export class GroupUpdateInviteMessage extends GroupUpdateMessage {
this.groupName = groupName; // not sure if getting an invite with an empty group name should make us drop an incoming group invite (and the keys associated to it too)
this.adminSignature = adminSignature;
this.memberAuthData = memberAuthData;
Preconditions.checkUin8tArrayOrThrow(
memberAuthData,
100,
'memberAuthData',
'GroupUpdateInviteMessage'
);
Preconditions.checkUin8tArrayOrThrow(
adminSignature,
32,
'adminSignature',
'GroupUpdateInviteMessage'
);
Preconditions.checkUin8tArrayOrThrow({
data: adminSignature,
expectedLength: 64,
varName: 'adminSignature',
context: this.constructor.toString(),
});
Preconditions.checkUin8tArrayOrThrow({
data: memberAuthData,
expectedLength: 100,
varName: 'memberAuthData',
context: this.constructor.toString(),
});
}
public dataProto(): SignalService.DataMessage {

@ -0,0 +1,37 @@
import { isEmpty } from 'lodash';
import { PubKey } from '../../types';
import { PreConditionFailed } from '../../utils/errors';
function checkUin8tArrayOrThrow({
context,
data,
expectedLength,
varName,
}: {
data: Uint8Array;
expectedLength: number;
varName: string;
context: string;
}) {
if (isEmpty(data) || data.length !== expectedLength) {
throw new PreConditionFailed(
`${varName} length should be ${expectedLength} for ctx:"${context}"`
);
}
}
function checkArrayHaveOnly05Pubkeys({
context,
arr,
varName,
}: {
arr: Array<string>;
varName: string;
context: string;
}) {
if (arr.some(v => !PubKey.is05Pubkey(v))) {
throw new PreConditionFailed(`${varName} did not contain only 05 pubkeys for ctx:"${context}"`);
}
}
export const Preconditions = { checkUin8tArrayOrThrow, checkArrayHaveOnly05Pubkeys };

@ -75,11 +75,18 @@ const initNewGroupInWrapper = createAsyncThunk(
if (!members.includes(us)) {
throw new PreConditionFailed('initNewGroupInWrapper needs us to be a member');
}
const uniqMembers = uniq(members);
if (members.some(k => !PubKey.is05Pubkey(k))) {
throw new PreConditionFailed('initNewGroupInWrapper only works with members being pubkeys');
}
const uniqMembers = uniq(members) as Array<PubkeyType>; // the if just above ensures that this is fine
const newGroup = await UserGroupsWrapperActions.createGroup();
const groupPk = newGroup.pubkeyHex;
try {
const groupSecretKey = newGroup.secretKey;
if (!groupSecretKey) {
throw new Error('groupSecretKey was empty just after creation.');
}
newGroup.name = groupName; // this will be used by the linked devices until they fetch the info from the groups swarm
// the `GroupSync` below will need the secretKey of the group to be saved in the wrapper. So save it!
@ -130,6 +137,7 @@ const initNewGroupInWrapper = createAsyncThunk(
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded(groupPk);
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
throw new Error('failed to pushChangesToGroupSwarmIfNeeded');
}
await convo.unhideIfNeeded();
@ -138,6 +146,23 @@ const initNewGroupInWrapper = createAsyncThunk(
convo.updateLastMessage();
dispatch(resetOverlayMode());
await openConversationWithMessages({ conversationKey: groupPk, messageId: null });
// everything is setup for this group, we now need to send the invites to every members, privately and asynchronously, and gracefully handle errors with toasts.
const inviteDetails = await getGroupInvitesMessages({
groupName,
membersFromWrapper,
secretKey: groupSecretKey,
groupPk,
});
void inviteDetails.map(async detail => {
await getMessageQueue().sendToPubKeyNonDurably({
message: detail.invite,
namespace: SnodeNamespaces.Default,
pubkey: PubKey.cast(detail.member),
});
console.log(`sending invite message to ${detail.member}`);
});
return { groupPk: newGroup.pubkeyHex, infos, members: membersFromWrapper };
} catch (e) {
@ -328,18 +353,19 @@ const groupSlice = createSlice({
state.infos[groupPk] = infos;
state.members[groupPk] = members;
state.creationFromUIPending = false;
return state;
});
builder.addCase(initNewGroupInWrapper.rejected, state => {
window.log.error('a initNewGroupInWrapper was rejected');
state.creationFromUIPending = false;
throw new Error('initNewGroupInWrapper.rejected');
return state;
// FIXME delete the wrapper completely & corresponding dumps, and usergroups entry?
});
builder.addCase(initNewGroupInWrapper.pending, (state, _action) => {
state.creationFromUIPending = true;
window.log.error('a initNewGroupInWrapper is pending');
return state;
});
builder.addCase(loadMetaDumpsFromDB.fulfilled, (state, action) => {
const loaded = action.payload;
@ -347,9 +373,11 @@ const groupSlice = createSlice({
state.infos[element.groupPk] = element.infos;
state.members[element.groupPk] = element.members;
});
return state;
});
builder.addCase(loadMetaDumpsFromDB.rejected, () => {
builder.addCase(loadMetaDumpsFromDB.rejected, state => {
window.log.error('a loadMetaDumpsFromDB was rejected');
return state;
});
builder.addCase(refreshGroupDetailsFromWrapper.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
@ -367,6 +395,7 @@ const groupSlice = createSlice({
delete state.infos[groupPk];
delete state.members[groupPk];
}
return state;
});
builder.addCase(refreshGroupDetailsFromWrapper.rejected, () => {
window.log.error('a refreshGroupDetailsFromWrapper was rejected');

@ -2,6 +2,7 @@ import { expect } from 'chai';
import {
GroupMemberGet,
MetaGroupWrapperNode,
PubkeyType,
UserGroupsWrapperNode,
} from 'libsession_util_nodejs';
import { range } from 'lodash';
@ -15,7 +16,7 @@ function profilePicture() {
return { key: new Uint8Array(range(0, 32)), url: `${Math.random()}` };
}
function emptyMember(pubkeyHex: string): GroupMemberGet {
function emptyMember(pubkeyHex: PubkeyType): GroupMemberGet {
return {
inviteFailed: false,
invitePending: false,
@ -35,8 +36,8 @@ describe('libsession_metagroup', () => {
let us: TestUserKeyPairs;
let groupCreated: ReturnType<UserGroupsWrapperNode['createGroup']>;
let metaGroupWrapper: MetaGroupWrapperNode;
let member: string;
let member2: string;
let member: PubkeyType;
let member2: PubkeyType;
beforeEach(async () => {
us = await TestUtils.generateUserKeyPairs();

@ -12,6 +12,7 @@ import {
MergeSingle,
MetaGroupWrapperActionsCalls,
ProfilePicture,
PubkeyType,
UserConfigWrapperActionsCalls,
UserGroupsSet,
UserGroupsWrapperActionsCalls,
@ -407,11 +408,11 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
>,
/** GroupMembers wrapper specific actions */
memberGet: async (groupPk: GroupPubkeyType, pubkeyHex: string) =>
memberGet: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'memberGet', pubkeyHex]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['memberGet']>
>,
memberGetOrConstruct: async (groupPk: GroupPubkeyType, pubkeyHex: string) =>
memberGetOrConstruct: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,
'memberGetOrConstruct',
@ -421,29 +422,29 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'memberGetAll']) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['memberGetAll']>
>,
memberErase: async (groupPk: GroupPubkeyType, pubkeyHex: string) =>
memberErase: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'memberErase', pubkeyHex]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['memberErase']>
>,
memberSetAccepted: async (groupPk: GroupPubkeyType, pubkeyHex: string) =>
memberSetAccepted: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'memberSetAccepted', pubkeyHex]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['memberSetAccepted']>
>,
memberSetPromoted: async (groupPk: GroupPubkeyType, pubkeyHex: string, failed: boolean) =>
memberSetPromoted: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, failed: boolean) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,
'memberSetPromoted',
pubkeyHex,
failed,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetPromoted']>>,
memberSetInvited: async (groupPk: GroupPubkeyType, pubkeyHex: string, failed: boolean) =>
memberSetInvited: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, failed: boolean) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,
'memberSetInvited',
pubkeyHex,
failed,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetInvited']>>,
memberSetName: async (groupPk: GroupPubkeyType, pubkeyHex: string, name: string) =>
memberSetName: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, name: string) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,
'memberSetName',
@ -452,7 +453,7 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetName']>>,
memberSetProfilePicture: async (
groupPk: GroupPubkeyType,
pubkeyHex: string,
pubkeyHex: PubkeyType,
profilePicture: ProfilePicture
) =>
callLibSessionWorker([
@ -498,6 +499,12 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'decryptMessage', ciphertext]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['decryptMessage']>
>,
makeSwarmSubAccount: async (groupPk: GroupPubkeyType, memberPubkeyHex: PubkeyType) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,
'makeSwarmSubAccount',
memberPubkeyHex,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['makeSwarmSubAccount']>>,
};
export const callLibSessionWorker = async (

Loading…
Cancel
Save