import { uniq } from 'lodash'; import { CommunityInfo, LegacyGroupInfo, UserGroupsType } from 'session_util_wrapper'; import { Data } from '../../../data/data'; import { OpenGroupData } from '../../../data/opengroups'; import { ConversationModel } from '../../../models/conversation'; import { assertUnreachable, getCommunityInfoFromDBValues, getLegacyGroupInfoFromDBValues, } from '../../../types/sqlSharedTypes'; import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; import { OpenGroupUtils } from '../../apis/open_group_api/utils'; import { getConversationController } from '../../conversations'; /** * The key of this map is the convoId as stored in the database. */ const mappedCommunityWrapperValues = new Map(); /** * The key of this map is the convoId as stored in the database. So the legacy group 05 sessionID */ const mappedLegacyGroupWrapperValues = new Map(); /** * Update the UserGroupsWrapper with all the data is cares about from the database. */ async function insertAllUserGroupsIntoWrapper() { const convoIdsToInsert = uniq( getConversationController() .getConversations() .filter(isUserGroupToStoreInWrapper) .map(m => m.id) ); window.log.debug( `UserGroupsWrapper keep tracks of ${convoIdsToInsert.length} usergroups including groups and communities` ); for (let index = 0; index < convoIdsToInsert.length; index++) { const id = convoIdsToInsert[index]; await insertGroupsFromDBIntoWrapperAndRefresh(id); } } /** * Returns true if that conversation is an active group */ function isUserGroupToStoreInWrapper(convo: ConversationModel): boolean { return isCommunityToStoreInWrapper(convo) || isLegacyGroupToStoreInWrapper(convo); } function isCommunityToStoreInWrapper(convo: ConversationModel): boolean { return convo.isGroup() && convo.isPublic() && convo.isActive(); } function isLegacyGroupToStoreInWrapper(convo: ConversationModel): boolean { return ( convo.isGroup() && !convo.isPublic() && convo.id.startsWith('05') && // new closed groups won't start with 05 convo.isActive() && !convo.get('isKickedFromGroup') && !convo.get('left') ); } /** * Fetches the specified convo and updates the required field in the wrapper. * If that community does not exist in the wrapper, it is created before being updated. * Same applies for a legacy group. */ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise { const foundConvo = getConversationController().get(convoId); if (!foundConvo) { return; } if (!isUserGroupToStoreInWrapper(foundConvo)) { return; } const convoType: UserGroupsType = isCommunityToStoreInWrapper(foundConvo) ? 'Community' : 'LegacyGroup'; switch (convoType) { case 'Community': const asOpengroup = foundConvo.toOpenGroupV2(); const roomDetails = OpenGroupData.getV2OpenGroupRoomByRoomId(asOpengroup); if (!roomDetails) { return; } // we need to build the full URL with the pubkey so we can add it to the wrapper. Let's reuse the exposed method from the wrapper for that const fullUrl = await UserGroupsWrapperActions.buildFullUrlFromDetails( roomDetails.serverUrl, roomDetails.roomId, roomDetails.serverPublicKey ); const wrapperComm = getCommunityInfoFromDBValues({ priority: foundConvo.get('priority'), fullUrl, }); try { window.log.debug(`inserting into usergroup wrapper "${wrapperComm.fullUrl}"...`); // this does the create or the update of the matching existing community await UserGroupsWrapperActions.setCommunityByFullUrl( wrapperComm.fullUrl, wrapperComm.priority ); await refreshCachedUserGroup(convoId); } catch (e) { window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`); // we still let this go through } break; case 'LegacyGroup': const encryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(convoId); const wrapperLegacyGroup = getLegacyGroupInfoFromDBValues({ id: foundConvo.id, priority: foundConvo.get('priority'), members: foundConvo.get('members') || [], groupAdmins: foundConvo.get('groupAdmins') || [], expireTimer: foundConvo.get('expireTimer'), displayNameInProfile: foundConvo.get('displayNameInProfile'), encPubkeyHex: encryptionKeyPair?.publicHex || '', encSeckeyHex: encryptionKeyPair?.privateHex || '', lastJoinedTimestamp: foundConvo.get('lastJoinedTimestamp') || 0, }); try { window.log.debug(`inserting into usergroup wrapper "${foundConvo.id}"... }`); // this does the create or the update of the matching existing legacy group await UserGroupsWrapperActions.setLegacyGroup(wrapperLegacyGroup); await refreshCachedUserGroup(convoId); } catch (e) { window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`); // we still let this go through } break; default: assertUnreachable( convoType, `insertGroupsFromDBIntoWrapperAndRefresh case not handeld "${convoType}"` ); } } /** * @param duringAppStart set this to true if we should just fetch the cached value but not trigger a UI refresh of the corresponding conversation */ async function refreshCachedUserGroup(convoId: string, duringAppStart = false) { try { let refreshed = false; if (OpenGroupUtils.isOpenGroupV2(convoId)) { const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId); if (fromWrapper && fromWrapper.fullUrl) { mappedCommunityWrapperValues.set(convoId, fromWrapper); } refreshed = true; } else if (convoId.startsWith('05')) { // currently this should only be a legacy group here const fromWrapper = await UserGroupsWrapperActions.getLegacyGroup(convoId); if (fromWrapper) { mappedLegacyGroupWrapperValues.set(convoId, fromWrapper); } refreshed = true; } if (refreshed && !duringAppStart) { getConversationController() .get(convoId) ?.triggerUIRefresh(); } } catch (e) { window.log.info(`refreshMappedValue: not an opengroup convoID: ${convoId}`, e); } // TODOLATER handle the new closed groups once we got them ready } function getCommunityByConvoIdCached(convoId: string) { return mappedCommunityWrapperValues.get(convoId); } function getAllCommunitiesCached(): Array { return [...mappedCommunityWrapperValues.values()]; } /** * Removes the matching community from the wrapper and from the cached list of communities */ async function removeCommunityFromWrapper(convoId: string, fullUrlWithOrWithoutPubkey: string) { const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl( fullUrlWithOrWithoutPubkey ); if (fromWrapper) { await UserGroupsWrapperActions.eraseCommunityByFullUrl(fromWrapper.fullUrl); } mappedCommunityWrapperValues.delete(convoId); } function getLegacyGroupCached(convoId: string) { return mappedLegacyGroupWrapperValues.get(convoId); } function getAllLegacyGroups(): Array { return [...mappedLegacyGroupWrapperValues.values()]; } /** * Remove the matching legacy group from the wrapper and from the cached list of legacy groups */ async function removeLegacyGroupFromWrapper(groupPk: string) { const fromWrapper = await UserGroupsWrapperActions.getLegacyGroup(groupPk); if (fromWrapper) { await UserGroupsWrapperActions.eraseLegacyGroup(groupPk); } mappedLegacyGroupWrapperValues.delete(groupPk); } /** * This function can be used where there are things to do for all the types handled by this wrapper. * You can do a loop on all the types handled by this wrapper and have a switch using assertUnreachable to get errors when not every case is handled. * * * Note: Ideally, we'd like to have this type in the wrapper index.d.ts, but it would require it to be a index.ts instead, which causes a whole other bunch of issues because it is a native node module. */ function getUserGroupTypes(): Array { return ['Community', 'LegacyGroup']; } export const SessionUtilUserGroups = { // shared isUserGroupToStoreInWrapper, insertAllUserGroupsIntoWrapper, insertGroupsFromDBIntoWrapperAndRefresh, refreshCachedUserGroup, getUserGroupTypes, // communities isCommunityToStoreInWrapper, getAllCommunitiesCached, getCommunityByConvoIdCached, removeCommunityFromWrapper, // legacy group isLegacyGroupToStoreInWrapper, getLegacyGroupCached, getAllLegacyGroups, removeLegacyGroupFromWrapper, // a group can be removed but also just marked hidden, so only call this function when the group is completely removed // TODOLATER };