feat: handle isDestroyed flag + "you were removed from XXX"

pull/3052/head
Audric Ackermann 11 months ago
parent 176f125028
commit 2ec6c7f29c

@ -38,6 +38,7 @@
"done": "Done",
"youLeftTheGroup": "You have left the group.",
"youGotKickedFromGroup": "You were removed from the group.",
"youWereRemovedFrom": "You were removed from <b>$name$</b>.",
"unreadMessages": "Unread Messages",
"debugLogExplanation": "This log will be saved to your desktop.",
"reportIssue": "Report a Bug",

@ -24,6 +24,7 @@ import {
import {
useLibGroupInviteGroupName,
useLibGroupInvitePending,
useLibGroupKicked,
} from '../../state/selectors/userGroups';
import { LocalizerKeys } from '../../types/LocalizerKeys';
import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
@ -140,6 +141,7 @@ export const NoMessageInConversation = () => {
const nameToRender = useSelectedNicknameOrProfileNameOrShortenedPubkey() || '';
const isPrivate = useSelectedIsPrivate();
const isIncomingRequest = useIsIncomingRequest(selectedConversation);
const isKickedFromGroup = useLibGroupKicked(selectedConversation);
// groupV2 use its own invite logic as part of <GroupRequestExplanation />
if (
@ -160,11 +162,13 @@ export const NoMessageInConversation = () => {
} else if (isMe) {
localizedKey = 'noMessagesInNoteToSelf';
}
let dataTestId: SessionDataTestId = 'empty-conversation-notification';
if (isGroupV2 && isKickedFromGroup) {
localizedKey = 'youWereRemovedFrom';
dataTestId = 'group-control-message';
}
return (
<TextNotification
dataTestId="empty-conversation-notification"
html={window.i18n(localizedKey, [nameToRender])}
/>
<TextNotification dataTestId={dataTestId} html={window.i18n(localizedKey, [nameToRender])} />
);
};

@ -24,8 +24,11 @@ import {
triggerFakeAvatarUpdate,
} from '../../../../interactions/conversationInteractions';
import { Constants } from '../../../../session';
import { isDevProd } from '../../../../shared/env_vars';
import { ConvoHub } from '../../../../session/conversations';
import { PubKey } from '../../../../session/types';
import { isDevProd, isTestIntegration } from '../../../../shared/env_vars';
import { closeRightPanel } from '../../../../state/ducks/conversations';
import { updateConfirmModal } from '../../../../state/ducks/modalDialog';
import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section';
import {
useSelectedConversationKey,
@ -44,6 +47,7 @@ import { AttachmentTypeWithPath } from '../../../../types/Attachment';
import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment';
import { Avatar, AvatarSize } from '../../../avatar/Avatar';
import { Flex } from '../../../basic/Flex';
import { SessionButtonColor } from '../../../basic/SessionButton';
import { SpacerLG, SpacerMD, SpacerXL } from '../../../basic/Text';
import { PanelButtonGroup, PanelIconButton } from '../../../buttons';
import { MediaItemType } from '../../../lightbox/LightboxGallery';
@ -192,6 +196,43 @@ const StyledName = styled.h4`
font-size: var(--font-size-md);
`;
const DestroyGroupForAllMembersButton = () => {
const dispatch = useDispatch();
const groupPk = useSelectedConversationKey();
if (groupPk && PubKey.is03Pubkey(groupPk) && (isDevProd() || isTestIntegration())) {
return (
<PanelIconButton
dataTestId="delete-group-button"
iconType="delete"
color={'var(--danger-color)'}
text={window.i18n('editMenuDeleteGroup')}
onClick={() => {
dispatch(
// TODO build the right UI for this (just adding buttons for QA for now)
updateConfirmModal({
okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger,
title: window.i18n('editMenuDeleteGroup'),
conversationId: groupPk,
onClickOk: () => {
void ConvoHub.use().deleteGroup(groupPk, {
deleteAllMessagesOnSwarm: true,
emptyGroupButKeepAsKicked: false,
fromSyncMessage: false,
sendLeaveMessage: false,
forceDestroyForAllMembers: true,
});
},
})
);
}}
/>
);
}
return null;
};
export const OverlayRightPanelSettings = () => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]);
@ -354,14 +395,17 @@ export const OverlayRightPanelSettings = () => {
<MediaGallery documents={documents} media={media} />
{isGroup && (
<PanelIconButton
text={leaveGroupString}
dataTestId="leave-group-button"
disabled={isKickedFromGroup}
onClick={() => void deleteConvoAction()}
color={'var(--danger-color)'}
iconType={'delete'}
/>
<>
<PanelIconButton
text={leaveGroupString}
dataTestId="leave-group-button"
disabled={isKickedFromGroup}
onClick={() => void deleteConvoAction()}
color={'var(--danger-color)'}
iconType={'delete'}
/>
<DestroyGroupForAllMembersButton />
</>
)}
</PanelButtonGroup>
<SpacerLG />

@ -458,11 +458,20 @@ async function leaveGroupOrCommunityByConvoId({
status: ConversationInteractionStatus.Start,
});
await ConvoHub.use().deleteClosedGroup(conversationId, {
fromSyncMessage: false,
sendLeaveMessage,
emptyGroupButKeepAsKicked: false,
});
if (PubKey.is05Pubkey(conversationId)) {
await ConvoHub.use().deleteLegacyGroup(conversationId, {
fromSyncMessage: false,
sendLeaveMessage,
});
} else if (PubKey.is03Pubkey(conversationId)) {
await ConvoHub.use().deleteGroup(conversationId, {
fromSyncMessage: false,
sendLeaveMessage,
deleteAllMessagesOnSwarm: false,
emptyGroupButKeepAsKicked: false,
forceDestroyForAllMembers: false,
});
}
await clearConversationInteractionState({ conversationId });
} catch (err) {
window.log.warn(`showLeaveGroupByConvoId error: ${err}`);

2
ts/react.d.ts vendored

@ -107,6 +107,7 @@ declare module 'react' {
| 'conversation-request-explanation'
| 'group-invite-control-message'
| 'empty-conversation-notification'
| 'group-control-message'
// call notification types
| 'call-notification-missed-call'
@ -138,6 +139,7 @@ declare module 'react' {
| 'remove-moderators'
| 'add-moderators'
| 'edit-group-name'
| 'delete-group-button'
// SessionRadioGroup & SessionRadio
| 'password-input-confirm'

@ -768,11 +768,13 @@ async function handleClosedGroupMembersRemoved(
const ourPubKey = UserUtils.getOurPubKeyFromCache();
const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key);
if (wasCurrentUserKicked) {
if (!PubKey.is05Pubkey(groupPubKey)) {
throw new Error('handleClosedGroupMembersRemoved expected a 05 groupPk');
}
// we now want to remove everything related to a group when we get kicked from it.
await ConvoHub.use().deleteClosedGroup(groupPubKey, {
await ConvoHub.use().deleteLegacyGroup(groupPubKey, {
fromSyncMessage: false,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false, // legacy group case only here
});
} else {
// Note: we don't want to send a new encryption keypair when we get a member removed.
@ -854,21 +856,25 @@ function removeMemberFromZombies(
}
async function handleClosedGroupAdminMemberLeft(groupPublicKey: string, envelope: EnvelopePlus) {
if (!PubKey.is05Pubkey(groupPublicKey)) {
throw new Error('handleClosedGroupAdminMemberLeft excepted a 05 groupPk');
}
// if the admin was remove and we are the admin, it can only be voluntary
await ConvoHub.use().deleteClosedGroup(groupPublicKey, {
await ConvoHub.use().deleteLegacyGroup(groupPublicKey, {
fromSyncMessage: false,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
});
await IncomingMessageCache.removeFromCache(envelope);
}
async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopePlus) {
if (!PubKey.is05Pubkey(groupId)) {
throw new Error('handleClosedGroupLeftOurself excepted a 05 groupPk');
}
// if we ourself left. It can only mean that another of our device left the group and we just synced that message through the swarm
await ConvoHub.use().deleteClosedGroup(groupId, {
await ConvoHub.use().deleteLegacyGroup(groupId, {
fromSyncMessage: false,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
});
await IncomingMessageCache.removeFromCache(envelope);
}

@ -583,10 +583,9 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
);
const toLeaveFromDb = ConvoHub.use().get(toLeave.id);
// the wrapper told us that this group is not tracked, so even if we left/got kicked from it, remove it from the DB completely
await ConvoHub.use().deleteClosedGroup(toLeaveFromDb.id, {
await ConvoHub.use().deleteLegacyGroup(toLeaveFromDb.id, {
fromSyncMessage: true,
sendLeaveMessage: false, // this comes from the wrapper, so we must have left/got kicked from that group already and our device already handled it.
emptyGroupButKeepAsKicked: false,
});
}
@ -738,10 +737,12 @@ async function handleSingleGroupUpdateToLeave(toLeave: GroupPubkeyType) {
`About to deleteGroup ${toLeave} via handleSingleGroupUpdateToLeave as in DB but not in wrapper`
);
await ConvoHub.use().deleteClosedGroup(toLeave, {
await ConvoHub.use().deleteGroup(toLeave, {
fromSyncMessage: true,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
});
} catch (e) {
window.log.info('Failed to deleteClosedGroup with: ', e.message);

@ -50,10 +50,12 @@ async function handleLibSessionKickedMessage({
}
const inviteWasPending =
(await UserGroupsWrapperActions.getGroup(groupPk))?.invitePending || false;
await ConvoHub.use().deleteClosedGroup(groupPk, {
await ConvoHub.use().deleteGroup(groupPk, {
sendLeaveMessage: false,
fromSyncMessage: false,
emptyGroupButKeepAsKicked: !inviteWasPending,
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
});
}

@ -1,7 +1,7 @@
import ByteBuffer from 'bytebuffer';
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { from_hex } from 'libsodium-wrappers-sumo';
import { isEmpty } from 'lodash';
import { isEmpty, isString } from 'lodash';
import { AwaitedReturn, assertUnreachable } from '../../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { concatUInt8Array } from '../../crypto';
@ -310,7 +310,6 @@ abstract class AbstractRevokeSubRequest extends SnodeAPISubRequest {
public readonly groupPk: GroupPubkeyType;
public readonly timestamp: number;
public readonly revokeTokenHex: Array<string>;
protected readonly adminSecretKey: Uint8Array;
constructor({
@ -471,8 +470,49 @@ export class DeleteAllFromUserNodeSubRequest extends SnodeAPISubRequest {
}
}
// We don't need that one yet
// export class DeleteAllFromGroupNodeSubRequest extends DeleteAllFromUserNodeSubRequest {}
/**
* Delete all the messages and not the config messages for that group 03.
*/
export class DeleteAllFromGroupMsgNodeSubRequest extends SnodeAPISubRequest {
public method = 'delete_all' as const;
public readonly namespace = SnodeNamespaces.ClosedGroupMessages;
public readonly adminSecretKey: Uint8Array;
public readonly groupPk: GroupPubkeyType;
constructor(args: WithGroupPubkey & WithSecretKey) {
super();
this.groupPk = args.groupPk;
this.adminSecretKey = args.secretKey;
if (isEmpty(this.adminSecretKey)) {
throw new Error('DeleteAllFromGroupMsgNodeSubRequest needs an adminSecretKey');
}
}
public async buildAndSignParameters() {
const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({
method: this.method,
namespace: this.namespace,
group: { authData: null, pubkeyHex: this.groupPk, secretKey: this.adminSecretKey },
});
if (!signDetails) {
throw new Error(
`[DeleteAllFromGroupMsgNodeSubRequest] SnodeSignature.getSnodeSignatureParamsUs returned an empty result`
);
}
return {
method: this.method,
params: {
...signDetails,
namespace: this.namespace,
},
};
}
public loggingId(): string {
return `${this.method}-${ed25519Str(this.groupPk)}-${this.namespace}`;
}
}
export class DeleteHashesFromUserNodeSubRequest extends SnodeAPISubRequest {
public method = 'delete' as const;
@ -981,7 +1021,8 @@ export type RawSnodeSubRequests =
| UpdateExpiryOnNodeGroupSubRequest
| SubaccountRevokeSubRequest
| SubaccountUnrevokeSubRequest
| GetExpiriesFromNodeSubRequest;
| GetExpiriesFromNodeSubRequest
| DeleteAllFromGroupMsgNodeSubRequest;
export type BuiltSnodeSubRequests =
| ReturnType<RetrieveLegacyClosedGroupSubRequest['build']>
@ -1000,7 +1041,8 @@ export type BuiltSnodeSubRequests =
| AwaitedReturn<UpdateExpiryOnNodeGroupSubRequest['buildAndSignParameters']>
| AwaitedReturn<SubaccountRevokeSubRequest['buildAndSignParameters']>
| AwaitedReturn<SubaccountUnrevokeSubRequest['buildAndSignParameters']>
| AwaitedReturn<GetExpiriesFromNodeSubRequest['buildAndSignParameters']>;
| AwaitedReturn<GetExpiriesFromNodeSubRequest['buildAndSignParameters']>
| AwaitedReturn<DeleteAllFromGroupMsgNodeSubRequest['buildAndSignParameters']>;
export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string {
const { method, params } = request;
@ -1010,7 +1052,6 @@ export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string
return `${method}`;
case 'delete':
case 'delete_all':
case 'expire':
case 'get_expiries':
case 'get_swarm':
@ -1019,6 +1060,12 @@ export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string
const isUs = UserUtils.isUsFromCache(params.pubkey);
return `${method}-${isUs ? 'us' : ed25519Str(params.pubkey)}`;
}
case 'delete_all': {
const isUs = UserUtils.isUsFromCache(params.pubkey);
return `${method}-${isUs ? 'us' : ed25519Str(params.pubkey)}-${
isString(params.namespace) ? params.namespace : SnodeNamespace.toRole(params.namespace)
}}`;
}
case 'retrieve':
case 'store': {

@ -85,7 +85,7 @@ async function getGroupPromoteMessage({
type ParamsShared = {
groupPk: GroupPubkeyType;
namespace: SnodeNamespacesGroup;
method: 'retrieve' | 'store';
method: 'retrieve' | 'store' | 'delete_all';
};
type SigParamsAdmin = ParamsShared & {
@ -140,13 +140,16 @@ export type GroupDetailsNeededForSignature = Pick<
type StoreOrRetrieve = { method: 'store' | 'retrieve'; namespace: SnodeNamespacesGroup };
type DeleteHashes = { method: 'delete'; hashes: Array<string> };
type DeleteAllNonConfigs = { method: 'delete_all'; namespace: SnodeNamespacesGroup };
async function getSnodeGroupSignature({
group,
...args
}: {
group: GroupDetailsNeededForSignature | null;
} & (StoreOrRetrieve | DeleteHashes)): Promise<SigResultSubAccount | SigResultAdmin> {
} & (StoreOrRetrieve | DeleteHashes | DeleteAllNonConfigs)): Promise<
SigResultSubAccount | SigResultAdmin
> {
if (!group) {
throw new Error(`getSnodeGroupSignature: did not find group in wrapper`);
}
@ -155,6 +158,10 @@ async function getSnodeGroupSignature({
const groupSecretKey = secretKey && !isEmpty(secretKey) ? secretKey : null;
const groupAuthData = authData && !isEmpty(authData) ? authData : null;
if (args.method === 'delete_all' && isEmpty(secretKey)) {
throw new Error('getSnodeGroupSignature: delete_all needs an adminSecretKey');
}
if (groupSecretKey) {
if (args.method === 'delete') {
return getGroupSignatureByHashesParams({

@ -667,11 +667,20 @@ export class SwarmPolling {
window.log.debug(
`notPollingForGroupAsNotInWrapper ${ed25519Str(pubkey)} with reason:"${reason}"`
);
await ConvoHub.use().deleteClosedGroup(pubkey, {
fromSyncMessage: true,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
});
if (PubKey.is05Pubkey(pubkey)) {
await ConvoHub.use().deleteLegacyGroup(pubkey, {
fromSyncMessage: true,
sendLeaveMessage: false,
});
} else if (PubKey.is03Pubkey(pubkey)) {
await ConvoHub.use().deleteGroup(pubkey, {
fromSyncMessage: true,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
});
}
}
private loadGroupIds() {

@ -9,6 +9,7 @@ import { GroupPendingRemovals } from '../../../utils/job_runners/jobs/GroupPendi
import { LibSessionUtil } from '../../../utils/libsession/libsession_utils';
import { SnodeNamespaces } from '../namespaces';
import { RetrieveMessageItemWithNamespace } from '../types';
import { ConvoHub } from '../../../conversations';
/**
* This is a basic optimization to avoid running the logic when the `deleteBeforeSeconds`
@ -23,60 +24,70 @@ const lastAppliedRemoveAttachmentSentBeforeSeconds = new Map<GroupPubkeyType, nu
async function handleMetaMergeResults(groupPk: GroupPubkeyType) {
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
if (
infos &&
isNumber(infos.deleteBeforeSeconds) &&
isFinite(infos.deleteBeforeSeconds) &&
infos.deleteBeforeSeconds > 0 &&
(lastAppliedRemoveMsgSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) >
infos.deleteBeforeSeconds
) {
// delete any messages in this conversation sent before that timestamp (in seconds)
const deletedMsgIds = await Data.removeAllMessagesInConversationSentBefore({
deleteBeforeSeconds: infos.deleteBeforeSeconds,
conversationId: groupPk,
});
window.log.info(
`removeAllMessagesInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteBeforeSeconds}: `,
deletedMsgIds
);
window.inboxStore.dispatch(
messagesExpired(deletedMsgIds.map(messageId => ({ conversationKey: groupPk, messageId })))
);
lastAppliedRemoveMsgSentBeforeSeconds.set(groupPk, infos.deleteBeforeSeconds);
}
if (infos) {
if (infos.isDestroyed) {
window.log.info(`${ed25519Str(groupPk)} is marked as destroyed after merge. Removing it.`);
await ConvoHub.use().deleteGroup(groupPk, {
sendLeaveMessage: false,
fromSyncMessage: false,
emptyGroupButKeepAsKicked: true, // we just got something from the group's swarm, so it is not pendingInvite
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
});
} else {
if (
isNumber(infos.deleteBeforeSeconds) &&
isFinite(infos.deleteBeforeSeconds) &&
infos.deleteBeforeSeconds > 0 &&
(lastAppliedRemoveMsgSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) >
infos.deleteBeforeSeconds
) {
// delete any messages in this conversation sent before that timestamp (in seconds)
const deletedMsgIds = await Data.removeAllMessagesInConversationSentBefore({
deleteBeforeSeconds: infos.deleteBeforeSeconds,
conversationId: groupPk,
});
window.log.info(
`removeAllMessagesInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteBeforeSeconds}: `,
deletedMsgIds
);
window.inboxStore.dispatch(
messagesExpired(deletedMsgIds.map(messageId => ({ conversationKey: groupPk, messageId })))
);
lastAppliedRemoveMsgSentBeforeSeconds.set(groupPk, infos.deleteBeforeSeconds);
}
if (
infos &&
isNumber(infos.deleteAttachBeforeSeconds) &&
isFinite(infos.deleteAttachBeforeSeconds) &&
infos.deleteAttachBeforeSeconds > 0 &&
(lastAppliedRemoveAttachmentSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) >
infos.deleteAttachBeforeSeconds
) {
// delete any attachments in this conversation sent before that timestamp (in seconds)
const impactedMsgModels = await Data.getAllMessagesWithAttachmentsInConversationSentBefore({
deleteAttachBeforeSeconds: infos.deleteAttachBeforeSeconds,
conversationId: groupPk,
});
window.log.info(
`getAllMessagesWithAttachmentsInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteAttachBeforeSeconds}: impactedMsgModelsIds `,
impactedMsgModels.map(m => m.id)
);
if (
isNumber(infos.deleteAttachBeforeSeconds) &&
isFinite(infos.deleteAttachBeforeSeconds) &&
infos.deleteAttachBeforeSeconds > 0 &&
(lastAppliedRemoveAttachmentSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) >
infos.deleteAttachBeforeSeconds
) {
// delete any attachments in this conversation sent before that timestamp (in seconds)
const impactedMsgModels = await Data.getAllMessagesWithAttachmentsInConversationSentBefore({
deleteAttachBeforeSeconds: infos.deleteAttachBeforeSeconds,
conversationId: groupPk,
});
window.log.info(
`getAllMessagesWithAttachmentsInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteAttachBeforeSeconds}: impactedMsgModelsIds `,
impactedMsgModels.map(m => m.id)
);
for (let index = 0; index < impactedMsgModels.length; index++) {
const msg = impactedMsgModels[index];
for (let index = 0; index < impactedMsgModels.length; index++) {
const msg = impactedMsgModels[index];
// eslint-disable-next-line no-await-in-loop
// eslint-disable-next-line no-await-in-loop
await msg?.cleanup();
// eslint-disable-next-line no-await-in-loop
await msg?.cleanup();
}
lastAppliedRemoveAttachmentSentBeforeSeconds.set(groupPk, infos.deleteAttachBeforeSeconds);
}
}
const membersWithPendingRemovals =
await MetaGroupWrapperActions.memberGetAllPendingRemovals(groupPk);
if (membersWithPendingRemovals.length) {
await GroupPendingRemovals.addJob({ groupPk });
}
lastAppliedRemoveAttachmentSentBeforeSeconds.set(groupPk, infos.deleteAttachBeforeSeconds);
}
const membersWithPendingRemovals =
await MetaGroupWrapperActions.memberGetAllPendingRemovals(groupPk);
if (membersWithPendingRemovals.length) {
await GroupPendingRemovals.addJob({ groupPk });
}
}

@ -15,26 +15,34 @@ import { getOpenGroupManager } from '../apis/open_group_api/opengroupV2/OpenGrou
import { PubKey } from '../types';
import { getMessageQueue } from '..';
import { ConfigDumpData } from '../../data/configDump/configDump';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes';
import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../webworker/workers/browser/libsession_worker_interface';
import { OpenGroupUtils } from '../apis/open_group_api/utils';
import { getSwarmPollingInstance } from '../apis/snode_api';
import { DeleteAllFromGroupMsgNodeSubRequest } from '../apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage';
import { UserUtils } from '../utils';
import { ed25519Str } from '../utils/String';
import { PreConditionFailed } from '../utils/errors';
import { RunJobResult } from '../utils/job_runners/PersistedJob';
import { GroupSync } from '../utils/job_runners/jobs/GroupSyncJob';
import { UserSync } from '../utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../utils/libsession/libsession_utils';
import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts';
import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_utils_convo_info_volatile';
import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups';
import { ed25519Str } from '../utils/String';
import { groupInfoActions } from '../../state/ducks/metaGroups';
let instance: ConvoController | null;
@ -205,57 +213,143 @@ class ConvoController {
await conversation.commit();
}
public async deleteClosedGroup(
groupPk: string,
public async deleteLegacyGroup(
groupPk: PubkeyType,
{ sendLeaveMessage, fromSyncMessage }: DeleteOptions & { sendLeaveMessage: boolean }
) {
if (!PubKey.is05Pubkey(groupPk)) {
throw new PreConditionFailed('deleteLegacyGroup excepts a 05 group');
}
window.log.info(
`deleteLegacyGroup: ${ed25519Str(groupPk)}, sendLeaveMessage:${sendLeaveMessage}, fromSyncMessage:${fromSyncMessage}`
);
// this deletes all messages in the conversation
const conversation = await this.deleteConvoInitialChecks(groupPk, 'LegacyGroup', false);
if (!conversation || !conversation.isClosedGroup()) {
return;
}
// we don't need to keep polling anymore.
getSwarmPollingInstance().removePubkey(groupPk, 'deleteLegacyGroup');
// send the leave message before we delete everything for this group (including the key!)
if (sendLeaveMessage) {
await leaveClosedGroup(groupPk, fromSyncMessage);
}
await removeLegacyGroupFromWrappers(groupPk);
// we never keep a left legacy group. Only fully remove it.
await this.removeGroupOrCommunityFromDBAndRedux(groupPk);
await UserSync.queueNewJobIfNeeded();
}
public async deleteGroup(
groupPk: GroupPubkeyType,
{
sendLeaveMessage,
fromSyncMessage,
emptyGroupButKeepAsKicked,
}: DeleteOptions & { sendLeaveMessage: boolean; emptyGroupButKeepAsKicked: boolean }
deleteAllMessagesOnSwarm,
forceDestroyForAllMembers,
}: DeleteOptions & {
sendLeaveMessage: boolean;
emptyGroupButKeepAsKicked: boolean;
deleteAllMessagesOnSwarm: boolean;
forceDestroyForAllMembers: boolean;
}
) {
if (!PubKey.is03Pubkey(groupPk) && !PubKey.is05Pubkey(groupPk)) {
return;
if (!PubKey.is03Pubkey(groupPk)) {
throw new PreConditionFailed('deleteGroup excepts a 03-group');
}
const typeOfDelete: ConvoVolatileType = PubKey.is03Pubkey(groupPk) ? 'Group' : 'LegacyGroup';
window.log.info(
`deleteClosedGroup: ${ed25519Str(groupPk)}, sendLeaveMessage:${sendLeaveMessage}, fromSyncMessage:${fromSyncMessage}, emptyGroupButKeepAsKicked:${emptyGroupButKeepAsKicked}`
`deleteGroup: ${ed25519Str(groupPk)}, sendLeaveMessage:${sendLeaveMessage}, fromSyncMessage:${fromSyncMessage}, emptyGroupButKeepAsKicked:${emptyGroupButKeepAsKicked}, deleteAllMessagesOnSwarm:${deleteAllMessagesOnSwarm}, forceDestroyForAllMembers:${forceDestroyForAllMembers}`
);
// this deletes all messages in the conversation
const conversation = await this.deleteConvoInitialChecks(groupPk, typeOfDelete, false);
const conversation = await this.deleteConvoInitialChecks(groupPk, 'Group', false);
if (!conversation || !conversation.isClosedGroup()) {
return;
}
// we don't need to keep polling anymore.
getSwarmPollingInstance().removePubkey(groupPk, 'deleteClosedGroup');
getSwarmPollingInstance().removePubkey(groupPk, 'deleteGroup');
const group = await UserGroupsWrapperActions.getGroup(groupPk);
// send the leave message before we delete everything for this group (including the key!)
if (sendLeaveMessage) {
// Note: if we were kicked, we already lost the authdata/secretKey for it, so no need to try to send our message.
if (sendLeaveMessage && !group?.kicked) {
await leaveClosedGroup(groupPk, fromSyncMessage);
}
if (PubKey.is03Pubkey(groupPk)) {
// a group 03 can be removed fully or kept empty as kicked.
// when it was pendingInvite, we delete it fully,
// when it was not, we empty the group but keep it with the "you have been kicked" message
// Note: the pendingInvite=true case cannot really happen as we wouldn't be polling from that group (and so, not get the message kicking us )
if (emptyGroupButKeepAsKicked) {
window?.inboxStore?.dispatch(groupInfoActions.emptyGroupButKeepAsKicked({ groupPk }));
} else {
window?.inboxStore?.dispatch(groupInfoActions.destroyGroupDetails({ groupPk }));
// a group 03 can be removed fully or kept empty as kicked.
// when it was pendingInvite, we delete it fully,
// when it was not, we empty the group but keep it with the "you have been kicked" message
// Note: the pendingInvite=true case cannot really happen as we wouldn't be polling from that group (and so, not get the message kicking us)
if (emptyGroupButKeepAsKicked) {
// delete the secretKey/authData if we had it. If we need it for something, it has to be done before this call.
if (group) {
group.authData = null;
group.secretKey = null;
group.disappearingTimerSeconds = undefined;
group.kicked = true;
await UserGroupsWrapperActions.setGroup(group);
}
} else {
await removeLegacyGroupFromWrappers(groupPk);
}
// if we were kicked or sent our left message, we have nothing to do more with that group.
// Just delete everything related to it, not trying to add update message or send a left message.
if (!emptyGroupButKeepAsKicked) {
const us = UserUtils.getOurPubKeyStrFromCache();
const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk);
const otherAdminsCount = allMembers
.filter(m => m.admin || m.promoted)
.filter(m => m.pubkeyHex !== us).length;
const weAreLastAdmin = otherAdminsCount === 0;
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
const fromUserGroup = await UserGroupsWrapperActions.getGroup(groupPk);
if (!infos || !fromUserGroup || isEmpty(infos) || isEmpty(fromUserGroup)) {
throw new Error('deleteGroup: some required data not present');
}
const { secretKey } = fromUserGroup;
// check if we are the last admin
if (secretKey && !isEmpty(secretKey) && (weAreLastAdmin || forceDestroyForAllMembers)) {
const deleteAllMessagesSubRequest = deleteAllMessagesOnSwarm
? new DeleteAllFromGroupMsgNodeSubRequest({
groupPk,
secretKey,
})
: null;
// this marks the group info as deleted. We need to push those details
await MetaGroupWrapperActions.infoDestroy(groupPk);
const lastPushResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
supplementKeys: [],
deleteAllMessagesSubRequest,
});
if (lastPushResult !== RunJobResult.Success) {
throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`);
}
}
// this deletes the secretKey if we had it. If we need it for something, it has to be done before this call.
await UserGroupsWrapperActions.eraseGroup(groupPk);
// we are on the emptyGroupButKeepAsKicked=false case, so we remove it all
await this.removeGroupOrCommunityFromDBAndRedux(groupPk);
}
if (!fromSyncMessage) {
await UserSync.queueNewJobIfNeeded();
}
await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk);
// release the memory (and the current meta-dumps in memory for that group)
window.log.info(`freeing metagroup wrapper: ${ed25519Str(groupPk)}`);
await MetaGroupWrapperActions.free(groupPk);
// delete the dumps from the metagroup state only, not the details in the UserGroups wrapper itself.
await ConfigDumpData.deleteDumpFor(groupPk);
getSwarmPollingInstance().removePubkey(groupPk, 'deleteGroup');
window.inboxStore.dispatch(groupInfoActions.removeGroupDetailsFromSlice({ groupPk }));
await UserSync.queueNewJobIfNeeded();
}
public async deleteCommunity(convoId: string, options: DeleteOptions) {

@ -312,7 +312,7 @@ export async function testGuardNode(snode: Snode) {
response = await insecureNodeFetch(url, fetchOptions);
} catch (e) {
if (e.type === 'request-timeout') {
window?.log?.warn('test :,', ed25519Str(snode.pubkey_ed25519));
window?.log?.warn('testGuardNode request timedout for:', ed25519Str(snode.pubkey_ed25519));
}
if (e.code === 'ENETUNREACH') {
window?.log?.warn('no network on node,', snode);

@ -16,6 +16,7 @@ import {
} from '../apis/open_group_api/sogsv3/sogsV3SendMessage';
import {
BuiltSnodeSubRequests,
DeleteAllFromGroupMsgNodeSubRequest,
DeleteAllFromUserNodeSubRequest,
DeleteHashesFromGroupNodeSubRequest,
DeleteHashesFromUserNodeSubRequest,
@ -308,7 +309,8 @@ async function signSubRequests(
p instanceof RetrieveGroupSubRequest ||
p instanceof UpdateExpiryOnNodeUserSubRequest ||
p instanceof UpdateExpiryOnNodeGroupSubRequest ||
p instanceof GetExpiriesFromNodeSubRequest
p instanceof GetExpiriesFromNodeSubRequest ||
p instanceof DeleteAllFromGroupMsgNodeSubRequest
) {
return p.buildAndSignParameters();
}
@ -348,7 +350,11 @@ async function sendMessagesDataToSnode(
messagesHashes: messagesToDelete,
revokeSubRequest,
unrevokeSubRequest,
}: WithMessagesHashes & WithRevokeSubRequest,
deleteAllMessagesSubRequest,
}: WithMessagesHashes &
WithRevokeSubRequest & {
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
},
method: MethodBatchType
): Promise<NotEmptyArrayOfBatchResults> {
if (!asssociatedWith) {
@ -375,6 +381,7 @@ async function sendMessagesDataToSnode(
deleteHashesSubRequest,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,
]);
const targetNode = await SnodePool.getNodeFromSwarmOrThrow(asssociatedWith);
@ -564,10 +571,12 @@ async function sendEncryptedDataToSnode({
messagesHashesToDelete,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,
}: WithRevokeSubRequest & {
storeRequests: Array<StoreGroupConfigOrMessageSubRequest | StoreUserConfigSubRequest>;
destination: GroupPubkeyType | PubkeyType;
messagesHashesToDelete: Set<string> | null;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
}): Promise<NotEmptyArrayOfBatchResults | null> {
try {
const batchResults = await pRetry(
@ -579,6 +588,7 @@ async function sendEncryptedDataToSnode({
messagesHashes: [...(messagesHashesToDelete || [])],
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,
},
'sequence'
);

@ -7,6 +7,7 @@ import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { isSignInByLinking } from '../../../../util/storage';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
DeleteAllFromGroupMsgNodeSubRequest,
StoreGroupConfigOrMessageSubRequest,
StoreGroupExtraData,
} from '../../../apis/snode_api/SnodeRequestTypes';
@ -130,6 +131,7 @@ async function storeGroupUpdateMessages({
messagesHashesToDelete: null,
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
});
const expectedReplyLength = updateMessagesRequests.length; // each of those messages are sent as a subrequest
@ -151,9 +153,11 @@ async function pushChangesToGroupSwarmIfNeeded({
unrevokeSubRequest,
groupPk,
supplementKeys,
deleteAllMessagesSubRequest,
}: WithGroupPubkey &
WithRevokeSubRequest & {
supplementKeys: Array<Uint8Array>;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
}): Promise<RunJobResult> {
// save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc
await LibSessionUtil.saveDumpsToDb(groupPk);
@ -161,7 +165,13 @@ async function pushChangesToGroupSwarmIfNeeded({
await LibSessionUtil.pendingChangesForGroup(groupPk);
// If there are no pending changes then the job can just complete (next time something
// is updated we want to try and run immediately so don't schedule another run in this case)
if (isEmpty(pendingConfigData) && !supplementKeys.length) {
if (
isEmpty(pendingConfigData) &&
!supplementKeys.length &&
!revokeSubRequest &&
!unrevokeSubRequest &&
!deleteAllMessagesSubRequest
) {
return RunJobResult.Success;
}
@ -233,14 +243,16 @@ async function pushChangesToGroupSwarmIfNeeded({
messagesHashesToDelete: allOldHashes,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,
});
const expectedReplyLength =
pendingConfigRequests.length + // each of those messages are sent as a subrequest
keysEncryptedRequests.length + // each of those messages are sent as a subrequest
(allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single request
(revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single request
(unrevokeSubRequest ? 1 : 0); // we are sending all revoke updates as a single request
(allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single subrequest
(revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(unrevokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(deleteAllMessagesSubRequest ? 1 : 0); // a delete_all sub request is a single subrequest
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) {

@ -38,7 +38,6 @@ import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { RunJobResult } from '../../session/utils/job_runners/PersistedJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { SessionUtilConvoInfoVolatile } from '../../session/utils/libsession/libsession_utils_convo_info_volatile';
import { getUserED25519KeyPairBytes } from '../../session/utils/User';
import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import {
@ -52,7 +51,6 @@ import {
import { StateType } from '../reducer';
import { openConversationWithMessages } from './conversations';
import { resetLeftOverlayMode } from './section';
import { ed25519Str } from '../../session/utils/String';
type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message.
export type GroupState = {
@ -185,6 +183,7 @@ const initNewGroupInWrapper = createAsyncThunk(
revokeSubRequest: null,
unrevokeSubRequest: null,
supplementKeys: [],
deleteAllMessagesSubRequest: null,
});
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
@ -247,10 +246,12 @@ const initNewGroupInWrapper = createAsyncThunk(
await MetaGroupWrapperActions.infoDestroy(groupPk);
const foundConvo = ConvoHub.use().get(groupPk);
if (foundConvo) {
await ConvoHub.use().deleteClosedGroup(groupPk, {
await ConvoHub.use().deleteGroup(groupPk, {
fromSyncMessage: false,
sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
});
}
throw e;
@ -392,7 +393,6 @@ const loadMetaDumpsFromDB = createAsyncThunk(
/**
* This action is to be called when we get a merge event from the network.
* It refreshes the state of that particular group (info & members) with the state from the wrapper after the merge is done.
*
*/
const refreshGroupDetailsFromWrapper = createAsyncThunk(
'group/refreshGroupDetailsFromWrapper',
@ -415,69 +415,6 @@ const refreshGroupDetailsFromWrapper = createAsyncThunk(
}
);
const destroyGroupDetails = createAsyncThunk(
'group/destroyGroupDetails',
async ({ groupPk }: { groupPk: GroupPubkeyType }) => {
const us = UserUtils.getOurPubKeyStrFromCache();
const weAreAdmin = await checkWeAreAdmin(groupPk);
const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk);
const otherAdminsCount = allMembers
.filter(m => m.admin || m.promoted)
.filter(m => m.pubkeyHex !== us).length;
// we are the last admin promoted
if (weAreAdmin && otherAdminsCount === 0) {
// this marks the group info as deleted. We need to push those details
await MetaGroupWrapperActions.infoDestroy(groupPk);
const lastPushResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
supplementKeys: [],
});
if (lastPushResult !== RunJobResult.Success) {
throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`);
}
}
// this deletes the secretKey if we had it. If we need it for something, it has to be done before this call.
await UserGroupsWrapperActions.eraseGroup(groupPk);
await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk);
await ConfigDumpData.deleteDumpFor(groupPk);
getSwarmPollingInstance().removePubkey(groupPk, 'destroyGroupDetails');
return { groupPk };
}
);
const emptyGroupButKeepAsKicked = createAsyncThunk(
'group/emptyGroupButKeepAsKicked',
async ({ groupPk }: { groupPk: GroupPubkeyType }) => {
window.log.info(`emptyGroupButKeepAsKicked for ${ed25519Str(groupPk)}`);
getSwarmPollingInstance().removePubkey(groupPk, 'emptyGroupButKeepAsKicked');
// this deletes the secretKey if we had it. If we need it for something, it has to be done before this call.
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (group) {
group.authData = null;
group.secretKey = null;
group.disappearingTimerSeconds = undefined;
group.kicked = true;
await UserGroupsWrapperActions.setGroup(group);
}
await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk);
// release the memory (and the current meta-dumps in memory for that group)
await MetaGroupWrapperActions.free(groupPk);
// this deletes the dumps from the metagroup state only, not the details in the UserGroups wrapper itself.
await ConfigDumpData.deleteDumpFor(groupPk);
return { groupPk };
}
);
function validateMemberAddChange({
groupPk,
withHistory: addMembersWithHistory,
@ -747,6 +684,7 @@ async function handleMemberAddedFromUI({
groupPk,
supplementKeys,
...revokeUnrevokeParams,
deleteAllMessagesSubRequest: null,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
@ -868,6 +806,7 @@ async function handleMemberRemovedFromUI({
supplementKeys: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
@ -979,6 +918,7 @@ async function handleNameChangeFromUI({
supplementKeys: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
});
if (batchResult !== RunJobResult.Success) {
@ -1260,6 +1200,15 @@ const metaGroupSlice = createSlice({
) {
return applySendingStateChange({ changeType: 'promote', ...payload, state });
},
removeGroupDetailsFromSlice(
state: GroupState,
{ payload }: PayloadAction<{ groupPk: GroupPubkeyType }>
) {
delete state.infos[payload.groupPk];
delete state.members[payload.groupPk];
delete state.membersInviteSending[payload.groupPk];
delete state.membersPromoteSending[payload.groupPk];
},
},
extraReducers: builder => {
builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => {
@ -1316,22 +1265,7 @@ const metaGroupSlice = createSlice({
builder.addCase(refreshGroupDetailsFromWrapper.rejected, (_state, action) => {
window.log.error('a refreshGroupDetailsFromWrapper was rejected', action.error);
});
builder.addCase(destroyGroupDetails.fulfilled, (state, action) => {
const { groupPk } = action.payload;
window.log.info(`removed 03 from metagroup wrapper ${ed25519Str(groupPk)}`);
deleteGroupPkEntriesFromState(state, groupPk);
});
builder.addCase(destroyGroupDetails.rejected, (_state, action) => {
window.log.error('a destroyGroupDetails was rejected', action.error);
});
builder.addCase(emptyGroupButKeepAsKicked.fulfilled, (state, action) => {
const { groupPk } = action.payload;
window.log.info(`markedAsKicked 03 from metagroup wrapper ${ed25519Str(groupPk)}`);
deleteGroupPkEntriesFromState(state, groupPk);
});
builder.addCase(emptyGroupButKeepAsKicked.rejected, (_state, action) => {
window.log.error('a emptyGroupButKeepAsKicked was rejected', action.error);
});
builder.addCase(handleUserGroupUpdate.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
if (infos && members) {
@ -1437,8 +1371,6 @@ const metaGroupSlice = createSlice({
export const groupInfoActions = {
initNewGroupInWrapper,
loadMetaDumpsFromDB,
destroyGroupDetails,
emptyGroupButKeepAsKicked,
refreshGroupDetailsFromWrapper,
handleUserGroupUpdate,
currentDeviceGroupMembersChange,

@ -402,7 +402,7 @@ export function useSelectedNicknameOrProfileNameOrShortenedPubkey() {
return window.i18n('noteToSelf');
}
if (selectedId && PubKey.is03Pubkey(selectedId)) {
return libGroupName;
return libGroupName || profileName || shortenedPubkey;
}
return nickname || profileName || shortenedPubkey;
}

@ -608,6 +608,7 @@ export type LocalizerKeys =
| 'youLeftTheGroup'
| 'youSetYourDisappearingMessages'
| 'youWereInvitedToGroup'
| 'youWereRemovedFrom'
| 'yourSessionID'
| 'yourUniqueSessionID'
| 'zoomFactorSettingTitle';

@ -268,61 +268,6 @@ export function getLegacyGroupInfoFromDBValues({
return legacyGroup;
}
/**
* This function should only be used to update the libsession fields of a 03-group.
* Most of the fields tracked in the usergroup wrapper in libsession are actually not updated
* once the entry is created, but some of them needs to be updated.
*/
export function getGroupInfoFromDBValues({
id,
priority,
members: maybeMembers,
displayNameInProfile,
expirationMode,
expireTimer,
encPubkeyHex,
encSeckeyHex,
groupAdmins: maybeAdmins,
lastJoinedTimestamp,
}: {
id: string;
priority: number;
displayNameInProfile: string | undefined;
expirationMode: DisappearingMessageConversationModeType | undefined;
expireTimer: number | undefined;
encPubkeyHex: string;
encSeckeyHex: string;
members: string | Array<string>;
groupAdmins: string | Array<string>;
lastJoinedTimestamp: number;
}) {
const admins: Array<string> = maybeArrayJSONtoArray(maybeAdmins);
const members: Array<string> = maybeArrayJSONtoArray(maybeMembers);
const wrappedMembers: Array<LegacyGroupMemberInfo> = (members || []).map(m => {
return {
isAdmin: admins.includes(m),
pubkeyHex: m,
};
});
const legacyGroup: LegacyGroupInfo = {
pubkeyHex: id,
name: displayNameInProfile || '',
priority: priority || 0,
members: wrappedMembers,
disappearingTimerSeconds:
expirationMode && expirationMode !== 'off' && !!expireTimer && expireTimer > 0
? expireTimer
: 0,
encPubkey: !isEmpty(encPubkeyHex) ? from_hex(encPubkeyHex) : new Uint8Array(),
encSeckey: !isEmpty(encSeckeyHex) ? from_hex(encSeckeyHex) : new Uint8Array(),
joinedAtSeconds: Math.floor(lastJoinedTimestamp / 1000),
};
return legacyGroup;
}
/**
* This function can be used to make sure all the possible values as input of a switch as taken care off, without having a default case.
*

Loading…
Cancel
Save