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/state/ducks/groups.ts

221 lines
8.0 KiB
TypeScript

import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { GroupInfoGet, GroupMemberGet, GroupPubkeyType } from 'libsession_util_nodejs';
import { isEmpty, uniq } from 'lodash';
import { ConfigDumpData } from '../../data/configDump/configDump';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings';
import { getConversationController } from '../../session/conversations';
import { UserUtils } from '../../session/utils';
import { GroupSync } from '../../session/utils/job_runners/jobs/GroupConfigJob';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import {
getGroupPubkeyFromWrapperType,
isMetaWrapperType,
} from '../../webworker/workers/browser/libsession_worker_functions';
import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../webworker/workers/browser/libsession_worker_interface';
import { PreConditionFailed } from '../../session/utils/errors';
export type GroupState = {
infos: Record<GroupPubkeyType, GroupInfoGet>;
members: Record<GroupPubkeyType, Array<GroupMemberGet>>;
};
export const initialGroupState: GroupState = {
infos: {},
members: {},
};
type GroupDetailsUpdate = {
groupPk: GroupPubkeyType;
infos: GroupInfoGet;
members: Array<GroupMemberGet>;
};
const initNewGroupInWrapper = createAsyncThunk(
'group/initNewGroupInWrapper',
async ({
groupName,
members,
us,
}: {
groupName: string;
members: Array<string>;
us: string;
}): Promise<GroupDetailsUpdate> => {
if (!members.includes(us)) {
throw new PreConditionFailed('initNewGroupInWrapper needs us to be a member');
}
const uniqMembers = uniq(members);
try {
const newGroup = await UserGroupsWrapperActions.createGroup();
const groupPk = newGroup.pubkeyHex;
newGroup.name = groupName; // this will be used by the linked devices until they fetch the info from the groups swarm
await UserGroupsWrapperActions.setGroup(newGroup);
const ourEd25519KeypairBytes = await UserUtils.getUserED25519KeyPairBytes();
if (!ourEd25519KeypairBytes) {
throw new Error('Current user has no priv ed25519 key?');
}
const userEd25519Secretkey = ourEd25519KeypairBytes.privKeyBytes;
const groupEd2519Pk = HexString.fromHexString(groupPk).slice(1); // remove the 03 prefix (single byte once in hex form)
// dump is always empty when creating a new groupInfo
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64),
groupEd25519Secretkey: newGroup.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32),
});
for (let index = 0; index < uniqMembers.length; index++) {
const member = uniqMembers[index];
const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member);
if (created.pubkeyHex === us) {
await MetaGroupWrapperActions.memberSetPromoted(groupPk, created.pubkeyHex, false);
} else {
await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false);
}
}
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
if (!infos) {
throw new Error(`getInfos of ${groupPk} returned empty result even if it was just init.`);
}
infos.name = groupName;
await MetaGroupWrapperActions.infoSet(groupPk, infos);
const membersFromWrapper = await MetaGroupWrapperActions.memberGetAll(groupPk);
if (!membersFromWrapper || isEmpty(membersFromWrapper)) {
throw new Error(
`memberGetAll of ${groupPk} returned empty result even if it was just init.`
);
}
const convo = await getConversationController().getOrCreateAndWait(
groupPk,
ConversationTypeEnum.GROUPV3
);
await convo.setIsApproved(true, false);
// console.warn('store the v3 identityPrivatekeypair as part of the wrapper only?');
// // the sync below will need the secretKey of the group to be saved in the wrapper. So save it!
await UserGroupsWrapperActions.setGroup(newGroup);
await GroupSync.queueNewJobIfNeeded(groupPk);
// const updateGroupDetails: ClosedGroup.GroupInfo = {
// id: newGroup.pubkeyHex,
// name: groupDetails.groupName,
// members,
// admins: [us],
// activeAt: Date.now(),
// expireTimer: 0,
// };
// // 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(updateGroupDetails);
await convo.unhideIfNeeded();
convo.set({ active_at: Date.now() });
await convo.commit();
convo.updateLastMessage();
return { groupPk: newGroup.pubkeyHex, infos, members: membersFromWrapper };
} catch (e) {
throw e;
}
}
);
const loadDumpsFromDB = createAsyncThunk(
'group/loadDumpsFromDB',
async (): Promise<Array<GroupDetailsUpdate>> => {
const variantsWithoutData = await ConfigDumpData.getAllDumpsWithoutData();
const allUserGroups = await UserGroupsWrapperActions.getAllGroups();
const toReturn: Array<GroupDetailsUpdate> = [];
for (let index = 0; index < variantsWithoutData.length; index++) {
const { variant } = variantsWithoutData[index];
if (!isMetaWrapperType(variant)) {
continue;
}
const groupPk = getGroupPubkeyFromWrapperType(variant);
const foundInUserWrapper = allUserGroups.find(m => m.pubkeyHex === groupPk);
if (!foundInUserWrapper) {
continue;
}
try {
window.log.debug(
'loadDumpsFromDB loading from metagroup variant: ',
variant,
foundInUserWrapper.pubkeyHex
);
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
const members = await MetaGroupWrapperActions.memberGetAll(groupPk);
toReturn.push({ groupPk, infos, members });
// Annoyingly, the redux store is not initialized when this current funciton is called,
// so we need to init the group wrappers here, but load them in their redux slice later
} catch (e) {
// TODO should not throw in this case? we should probably just try to load what we manage to load
window.log.warn(
`initGroup of Group wrapper of variant ${variant} failed with ${e.message} `
);
// throw new Error(`initializeLibSessionUtilWrappers failed with ${e.message}`);
}
}
return toReturn;
}
);
/**
* This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server.
*/
const groupSlice = createSlice({
name: 'group',
initialState: initialGroupState,
reducers: {
updateGroupDetailsAfterMerge(state, action: PayloadAction<GroupDetailsUpdate>) {
const { groupPk, infos, members } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
},
},
extraReducers: builder => {
builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => {
const { groupPk, infos, members } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
});
builder.addCase(initNewGroupInWrapper.rejected, () => {
window.log.error('a initNewGroupInWrapper was rejected');
// FIXME delete the wrapper completely & correspondign dumps, and usergroups entry?
});
builder.addCase(loadDumpsFromDB.fulfilled, (state, action) => {
const loaded = action.payload;
loaded.forEach(element => {
state.infos[element.groupPk] = element.infos;
state.members[element.groupPk] = element.members;
});
});
builder.addCase(loadDumpsFromDB.rejected, () => {
window.log.error('a loadDumpsFromDB was rejected');
});
},
});
export const groupInfoActions = {
initNewGroupInWrapper,
loadDumpsFromDB,
...groupSlice.actions,
};
export const groupReducer = groupSlice.reducer;