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

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

@ -24,8 +24,11 @@ import {
triggerFakeAvatarUpdate, triggerFakeAvatarUpdate,
} from '../../../../interactions/conversationInteractions'; } from '../../../../interactions/conversationInteractions';
import { Constants } from '../../../../session'; 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 { closeRightPanel } from '../../../../state/ducks/conversations';
import { updateConfirmModal } from '../../../../state/ducks/modalDialog';
import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section'; import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section';
import { import {
useSelectedConversationKey, useSelectedConversationKey,
@ -44,6 +47,7 @@ import { AttachmentTypeWithPath } from '../../../../types/Attachment';
import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment'; import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment';
import { Avatar, AvatarSize } from '../../../avatar/Avatar'; import { Avatar, AvatarSize } from '../../../avatar/Avatar';
import { Flex } from '../../../basic/Flex'; import { Flex } from '../../../basic/Flex';
import { SessionButtonColor } from '../../../basic/SessionButton';
import { SpacerLG, SpacerMD, SpacerXL } from '../../../basic/Text'; import { SpacerLG, SpacerMD, SpacerXL } from '../../../basic/Text';
import { PanelButtonGroup, PanelIconButton } from '../../../buttons'; import { PanelButtonGroup, PanelIconButton } from '../../../buttons';
import { MediaItemType } from '../../../lightbox/LightboxGallery'; import { MediaItemType } from '../../../lightbox/LightboxGallery';
@ -192,6 +196,43 @@ const StyledName = styled.h4`
font-size: var(--font-size-md); 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 = () => { export const OverlayRightPanelSettings = () => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]); const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]); const [media, setMedia] = useState<Array<MediaItemType>>([]);
@ -354,14 +395,17 @@ export const OverlayRightPanelSettings = () => {
<MediaGallery documents={documents} media={media} /> <MediaGallery documents={documents} media={media} />
{isGroup && ( {isGroup && (
<PanelIconButton <>
text={leaveGroupString} <PanelIconButton
dataTestId="leave-group-button" text={leaveGroupString}
disabled={isKickedFromGroup} dataTestId="leave-group-button"
onClick={() => void deleteConvoAction()} disabled={isKickedFromGroup}
color={'var(--danger-color)'} onClick={() => void deleteConvoAction()}
iconType={'delete'} color={'var(--danger-color)'}
/> iconType={'delete'}
/>
<DestroyGroupForAllMembersButton />
</>
)} )}
</PanelButtonGroup> </PanelButtonGroup>
<SpacerLG /> <SpacerLG />

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

2
ts/react.d.ts vendored

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

@ -768,11 +768,13 @@ async function handleClosedGroupMembersRemoved(
const ourPubKey = UserUtils.getOurPubKeyFromCache(); const ourPubKey = UserUtils.getOurPubKeyFromCache();
const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key); const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key);
if (wasCurrentUserKicked) { 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. // 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, fromSyncMessage: false,
sendLeaveMessage: false, sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false, // legacy group case only here
}); });
} else { } else {
// Note: we don't want to send a new encryption keypair when we get a member removed. // 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) { 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 // 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, fromSyncMessage: false,
sendLeaveMessage: false, sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
}); });
await IncomingMessageCache.removeFromCache(envelope); await IncomingMessageCache.removeFromCache(envelope);
} }
async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopePlus) { 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 // 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, fromSyncMessage: false,
sendLeaveMessage: false, sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false,
}); });
await IncomingMessageCache.removeFromCache(envelope); await IncomingMessageCache.removeFromCache(envelope);
} }

@ -583,10 +583,9 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
); );
const toLeaveFromDb = ConvoHub.use().get(toLeave.id); 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 // 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, 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. 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` `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, fromSyncMessage: true,
sendLeaveMessage: false, sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false, emptyGroupButKeepAsKicked: false,
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
}); });
} catch (e) { } catch (e) {
window.log.info('Failed to deleteClosedGroup with: ', e.message); window.log.info('Failed to deleteClosedGroup with: ', e.message);

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

@ -1,7 +1,7 @@
import ByteBuffer from 'bytebuffer'; import ByteBuffer from 'bytebuffer';
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { from_hex } from 'libsodium-wrappers-sumo'; import { from_hex } from 'libsodium-wrappers-sumo';
import { isEmpty } from 'lodash'; import { isEmpty, isString } from 'lodash';
import { AwaitedReturn, assertUnreachable } from '../../../types/sqlSharedTypes'; import { AwaitedReturn, assertUnreachable } from '../../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { concatUInt8Array } from '../../crypto'; import { concatUInt8Array } from '../../crypto';
@ -310,7 +310,6 @@ abstract class AbstractRevokeSubRequest extends SnodeAPISubRequest {
public readonly groupPk: GroupPubkeyType; public readonly groupPk: GroupPubkeyType;
public readonly timestamp: number; public readonly timestamp: number;
public readonly revokeTokenHex: Array<string>; public readonly revokeTokenHex: Array<string>;
protected readonly adminSecretKey: Uint8Array; protected readonly adminSecretKey: Uint8Array;
constructor({ 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 { export class DeleteHashesFromUserNodeSubRequest extends SnodeAPISubRequest {
public method = 'delete' as const; public method = 'delete' as const;
@ -981,7 +1021,8 @@ export type RawSnodeSubRequests =
| UpdateExpiryOnNodeGroupSubRequest | UpdateExpiryOnNodeGroupSubRequest
| SubaccountRevokeSubRequest | SubaccountRevokeSubRequest
| SubaccountUnrevokeSubRequest | SubaccountUnrevokeSubRequest
| GetExpiriesFromNodeSubRequest; | GetExpiriesFromNodeSubRequest
| DeleteAllFromGroupMsgNodeSubRequest;
export type BuiltSnodeSubRequests = export type BuiltSnodeSubRequests =
| ReturnType<RetrieveLegacyClosedGroupSubRequest['build']> | ReturnType<RetrieveLegacyClosedGroupSubRequest['build']>
@ -1000,7 +1041,8 @@ export type BuiltSnodeSubRequests =
| AwaitedReturn<UpdateExpiryOnNodeGroupSubRequest['buildAndSignParameters']> | AwaitedReturn<UpdateExpiryOnNodeGroupSubRequest['buildAndSignParameters']>
| AwaitedReturn<SubaccountRevokeSubRequest['buildAndSignParameters']> | AwaitedReturn<SubaccountRevokeSubRequest['buildAndSignParameters']>
| AwaitedReturn<SubaccountUnrevokeSubRequest['buildAndSignParameters']> | AwaitedReturn<SubaccountUnrevokeSubRequest['buildAndSignParameters']>
| AwaitedReturn<GetExpiriesFromNodeSubRequest['buildAndSignParameters']>; | AwaitedReturn<GetExpiriesFromNodeSubRequest['buildAndSignParameters']>
| AwaitedReturn<DeleteAllFromGroupMsgNodeSubRequest['buildAndSignParameters']>;
export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string { export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string {
const { method, params } = request; const { method, params } = request;
@ -1010,7 +1052,6 @@ export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string
return `${method}`; return `${method}`;
case 'delete': case 'delete':
case 'delete_all':
case 'expire': case 'expire':
case 'get_expiries': case 'get_expiries':
case 'get_swarm': case 'get_swarm':
@ -1019,6 +1060,12 @@ export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string
const isUs = UserUtils.isUsFromCache(params.pubkey); const isUs = UserUtils.isUsFromCache(params.pubkey);
return `${method}-${isUs ? 'us' : ed25519Str(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 'retrieve':
case 'store': { case 'store': {

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

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

@ -9,6 +9,7 @@ import { GroupPendingRemovals } from '../../../utils/job_runners/jobs/GroupPendi
import { LibSessionUtil } from '../../../utils/libsession/libsession_utils'; import { LibSessionUtil } from '../../../utils/libsession/libsession_utils';
import { SnodeNamespaces } from '../namespaces'; import { SnodeNamespaces } from '../namespaces';
import { RetrieveMessageItemWithNamespace } from '../types'; import { RetrieveMessageItemWithNamespace } from '../types';
import { ConvoHub } from '../../../conversations';
/** /**
* This is a basic optimization to avoid running the logic when the `deleteBeforeSeconds` * 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) { async function handleMetaMergeResults(groupPk: GroupPubkeyType) {
const infos = await MetaGroupWrapperActions.infoGet(groupPk); const infos = await MetaGroupWrapperActions.infoGet(groupPk);
if ( if (infos) {
infos && if (infos.isDestroyed) {
isNumber(infos.deleteBeforeSeconds) && window.log.info(`${ed25519Str(groupPk)} is marked as destroyed after merge. Removing it.`);
isFinite(infos.deleteBeforeSeconds) && await ConvoHub.use().deleteGroup(groupPk, {
infos.deleteBeforeSeconds > 0 && sendLeaveMessage: false,
(lastAppliedRemoveMsgSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) > fromSyncMessage: false,
infos.deleteBeforeSeconds emptyGroupButKeepAsKicked: true, // we just got something from the group's swarm, so it is not pendingInvite
) { deleteAllMessagesOnSwarm: false,
// delete any messages in this conversation sent before that timestamp (in seconds) forceDestroyForAllMembers: false,
const deletedMsgIds = await Data.removeAllMessagesInConversationSentBefore({ });
deleteBeforeSeconds: infos.deleteBeforeSeconds, } else {
conversationId: groupPk, if (
}); isNumber(infos.deleteBeforeSeconds) &&
window.log.info( isFinite(infos.deleteBeforeSeconds) &&
`removeAllMessagesInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteBeforeSeconds}: `, infos.deleteBeforeSeconds > 0 &&
deletedMsgIds (lastAppliedRemoveMsgSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) >
); infos.deleteBeforeSeconds
window.inboxStore.dispatch( ) {
messagesExpired(deletedMsgIds.map(messageId => ({ conversationKey: groupPk, messageId }))) // delete any messages in this conversation sent before that timestamp (in seconds)
); const deletedMsgIds = await Data.removeAllMessagesInConversationSentBefore({
lastAppliedRemoveMsgSentBeforeSeconds.set(groupPk, infos.deleteBeforeSeconds); 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 ( if (
infos && isNumber(infos.deleteAttachBeforeSeconds) &&
isNumber(infos.deleteAttachBeforeSeconds) && isFinite(infos.deleteAttachBeforeSeconds) &&
isFinite(infos.deleteAttachBeforeSeconds) && infos.deleteAttachBeforeSeconds > 0 &&
infos.deleteAttachBeforeSeconds > 0 && (lastAppliedRemoveAttachmentSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) >
(lastAppliedRemoveAttachmentSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) > infos.deleteAttachBeforeSeconds
infos.deleteAttachBeforeSeconds ) {
) { // delete any attachments in this conversation sent before that timestamp (in seconds)
// delete any attachments in this conversation sent before that timestamp (in seconds) const impactedMsgModels = await Data.getAllMessagesWithAttachmentsInConversationSentBefore({
const impactedMsgModels = await Data.getAllMessagesWithAttachmentsInConversationSentBefore({ deleteAttachBeforeSeconds: infos.deleteAttachBeforeSeconds,
deleteAttachBeforeSeconds: infos.deleteAttachBeforeSeconds, conversationId: groupPk,
conversationId: groupPk, });
}); window.log.info(
window.log.info( `getAllMessagesWithAttachmentsInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteAttachBeforeSeconds}: impactedMsgModelsIds `,
`getAllMessagesWithAttachmentsInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteAttachBeforeSeconds}: impactedMsgModelsIds `, impactedMsgModels.map(m => m.id)
impactedMsgModels.map(m => m.id) );
);
for (let index = 0; index < impactedMsgModels.length; index++) { for (let index = 0; index < impactedMsgModels.length; index++) {
const msg = impactedMsgModels[index]; const msg = impactedMsgModels[index];
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
// eslint-disable-next-line no-await-in-loop await msg?.cleanup();
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 { PubKey } from '../types';
import { getMessageQueue } from '..'; import { getMessageQueue } from '..';
import { ConfigDumpData } from '../../data/configDump/configDump';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions'; import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes'; import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes';
import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations'; import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { assertUnreachable } from '../../types/sqlSharedTypes'; 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 { OpenGroupUtils } from '../apis/open_group_api/utils';
import { getSwarmPollingInstance } from '../apis/snode_api'; import { getSwarmPollingInstance } from '../apis/snode_api';
import { DeleteAllFromGroupMsgNodeSubRequest } from '../apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime'; import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage'; import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage';
import { UserUtils } from '../utils'; 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 { UserSync } from '../utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../utils/libsession/libsession_utils'; import { LibSessionUtil } from '../utils/libsession/libsession_utils';
import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts'; import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts';
import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_utils_convo_info_volatile'; import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_utils_convo_info_volatile';
import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups'; import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups';
import { ed25519Str } from '../utils/String'; import { groupInfoActions } from '../../state/ducks/metaGroups';
let instance: ConvoController | null; let instance: ConvoController | null;
@ -205,57 +213,143 @@ class ConvoController {
await conversation.commit(); await conversation.commit();
} }
public async deleteClosedGroup( public async deleteLegacyGroup(
groupPk: string, 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, sendLeaveMessage,
fromSyncMessage, fromSyncMessage,
emptyGroupButKeepAsKicked, emptyGroupButKeepAsKicked,
}: DeleteOptions & { sendLeaveMessage: boolean; emptyGroupButKeepAsKicked: boolean } deleteAllMessagesOnSwarm,
forceDestroyForAllMembers,
}: DeleteOptions & {
sendLeaveMessage: boolean;
emptyGroupButKeepAsKicked: boolean;
deleteAllMessagesOnSwarm: boolean;
forceDestroyForAllMembers: boolean;
}
) { ) {
if (!PubKey.is03Pubkey(groupPk) && !PubKey.is05Pubkey(groupPk)) { if (!PubKey.is03Pubkey(groupPk)) {
return; throw new PreConditionFailed('deleteGroup excepts a 03-group');
} }
const typeOfDelete: ConvoVolatileType = PubKey.is03Pubkey(groupPk) ? 'Group' : 'LegacyGroup';
window.log.info( 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 // 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()) { if (!conversation || !conversation.isClosedGroup()) {
return; return;
} }
// we don't need to keep polling anymore. // 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!) // 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); await leaveClosedGroup(groupPk, fromSyncMessage);
} }
if (PubKey.is03Pubkey(groupPk)) { // a group 03 can be removed fully or kept empty as kicked.
// a group 03 can be removed fully or kept empty as kicked. // when it was pendingInvite, we delete it fully,
// 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
// 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)
// 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) {
if (emptyGroupButKeepAsKicked) { // delete the secretKey/authData if we had it. If we need it for something, it has to be done before this call.
window?.inboxStore?.dispatch(groupInfoActions.emptyGroupButKeepAsKicked({ groupPk })); if (group) {
} else { group.authData = null;
window?.inboxStore?.dispatch(groupInfoActions.destroyGroupDetails({ groupPk })); group.secretKey = null;
group.disappearingTimerSeconds = undefined;
group.kicked = true;
await UserGroupsWrapperActions.setGroup(group);
} }
} else { } else {
await removeLegacyGroupFromWrappers(groupPk); const us = UserUtils.getOurPubKeyStrFromCache();
} const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk);
// if we were kicked or sent our left message, we have nothing to do more with that group. const otherAdminsCount = allMembers
// Just delete everything related to it, not trying to add update message or send a left message. .filter(m => m.admin || m.promoted)
if (!emptyGroupButKeepAsKicked) { .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); await this.removeGroupOrCommunityFromDBAndRedux(groupPk);
} }
if (!fromSyncMessage) { await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk);
await UserSync.queueNewJobIfNeeded(); // 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) { public async deleteCommunity(convoId: string, options: DeleteOptions) {

@ -312,7 +312,7 @@ export async function testGuardNode(snode: Snode) {
response = await insecureNodeFetch(url, fetchOptions); response = await insecureNodeFetch(url, fetchOptions);
} catch (e) { } catch (e) {
if (e.type === 'request-timeout') { 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') { if (e.code === 'ENETUNREACH') {
window?.log?.warn('no network on node,', snode); window?.log?.warn('no network on node,', snode);

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

@ -7,6 +7,7 @@ import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { isSignInByLinking } from '../../../../util/storage'; import { isSignInByLinking } from '../../../../util/storage';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { import {
DeleteAllFromGroupMsgNodeSubRequest,
StoreGroupConfigOrMessageSubRequest, StoreGroupConfigOrMessageSubRequest,
StoreGroupExtraData, StoreGroupExtraData,
} from '../../../apis/snode_api/SnodeRequestTypes'; } from '../../../apis/snode_api/SnodeRequestTypes';
@ -130,6 +131,7 @@ async function storeGroupUpdateMessages({
messagesHashesToDelete: null, messagesHashesToDelete: null,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
}); });
const expectedReplyLength = updateMessagesRequests.length; // each of those messages are sent as a subrequest const expectedReplyLength = updateMessagesRequests.length; // each of those messages are sent as a subrequest
@ -151,9 +153,11 @@ async function pushChangesToGroupSwarmIfNeeded({
unrevokeSubRequest, unrevokeSubRequest,
groupPk, groupPk,
supplementKeys, supplementKeys,
deleteAllMessagesSubRequest,
}: WithGroupPubkey & }: WithGroupPubkey &
WithRevokeSubRequest & { WithRevokeSubRequest & {
supplementKeys: Array<Uint8Array>; supplementKeys: Array<Uint8Array>;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
}): Promise<RunJobResult> { }): 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 // 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); await LibSessionUtil.saveDumpsToDb(groupPk);
@ -161,7 +165,13 @@ async function pushChangesToGroupSwarmIfNeeded({
await LibSessionUtil.pendingChangesForGroup(groupPk); await LibSessionUtil.pendingChangesForGroup(groupPk);
// If there are no pending changes then the job can just complete (next time something // 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) // 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; return RunJobResult.Success;
} }
@ -233,14 +243,16 @@ async function pushChangesToGroupSwarmIfNeeded({
messagesHashesToDelete: allOldHashes, messagesHashesToDelete: allOldHashes,
revokeSubRequest, revokeSubRequest,
unrevokeSubRequest, unrevokeSubRequest,
deleteAllMessagesSubRequest,
}); });
const expectedReplyLength = const expectedReplyLength =
pendingConfigRequests.length + // each of those messages are sent as a subrequest pendingConfigRequests.length + // each of those messages are sent as a subrequest
keysEncryptedRequests.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 (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 request (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 request (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 // 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) { 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 { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { RunJobResult } from '../../session/utils/job_runners/PersistedJob'; import { RunJobResult } from '../../session/utils/job_runners/PersistedJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils'; 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 { getUserED25519KeyPairBytes } from '../../session/utils/User';
import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { import {
@ -52,7 +51,6 @@ import {
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { openConversationWithMessages } from './conversations'; import { openConversationWithMessages } from './conversations';
import { resetLeftOverlayMode } from './section'; 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. type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message.
export type GroupState = { export type GroupState = {
@ -185,6 +183,7 @@ const initNewGroupInWrapper = createAsyncThunk(
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
supplementKeys: [], supplementKeys: [],
deleteAllMessagesSubRequest: null,
}); });
if (result !== RunJobResult.Success) { if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed'); window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
@ -247,10 +246,12 @@ const initNewGroupInWrapper = createAsyncThunk(
await MetaGroupWrapperActions.infoDestroy(groupPk); await MetaGroupWrapperActions.infoDestroy(groupPk);
const foundConvo = ConvoHub.use().get(groupPk); const foundConvo = ConvoHub.use().get(groupPk);
if (foundConvo) { if (foundConvo) {
await ConvoHub.use().deleteClosedGroup(groupPk, { await ConvoHub.use().deleteGroup(groupPk, {
fromSyncMessage: false, fromSyncMessage: false,
sendLeaveMessage: false, sendLeaveMessage: false,
emptyGroupButKeepAsKicked: false, emptyGroupButKeepAsKicked: false,
deleteAllMessagesOnSwarm: false,
forceDestroyForAllMembers: false,
}); });
} }
throw e; throw e;
@ -392,7 +393,6 @@ const loadMetaDumpsFromDB = createAsyncThunk(
/** /**
* This action is to be called when we get a merge event from the network. * 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. * It refreshes the state of that particular group (info & members) with the state from the wrapper after the merge is done.
*
*/ */
const refreshGroupDetailsFromWrapper = createAsyncThunk( const refreshGroupDetailsFromWrapper = createAsyncThunk(
'group/refreshGroupDetailsFromWrapper', '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({ function validateMemberAddChange({
groupPk, groupPk,
withHistory: addMembersWithHistory, withHistory: addMembersWithHistory,
@ -747,6 +684,7 @@ async function handleMemberAddedFromUI({
groupPk, groupPk,
supplementKeys, supplementKeys,
...revokeUnrevokeParams, ...revokeUnrevokeParams,
deleteAllMessagesSubRequest: null,
}); });
if (sequenceResult !== RunJobResult.Success) { if (sequenceResult !== RunJobResult.Success) {
throw new Error( throw new Error(
@ -868,6 +806,7 @@ async function handleMemberRemovedFromUI({
supplementKeys: [], supplementKeys: [],
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
}); });
if (sequenceResult !== RunJobResult.Success) { if (sequenceResult !== RunJobResult.Success) {
throw new Error( throw new Error(
@ -979,6 +918,7 @@ async function handleNameChangeFromUI({
supplementKeys: [], supplementKeys: [],
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
}); });
if (batchResult !== RunJobResult.Success) { if (batchResult !== RunJobResult.Success) {
@ -1260,6 +1200,15 @@ const metaGroupSlice = createSlice({
) { ) {
return applySendingStateChange({ changeType: 'promote', ...payload, state }); 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 => { extraReducers: builder => {
builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => { builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => {
@ -1316,22 +1265,7 @@ const metaGroupSlice = createSlice({
builder.addCase(refreshGroupDetailsFromWrapper.rejected, (_state, action) => { builder.addCase(refreshGroupDetailsFromWrapper.rejected, (_state, action) => {
window.log.error('a refreshGroupDetailsFromWrapper was rejected', action.error); 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) => { builder.addCase(handleUserGroupUpdate.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload; const { infos, members, groupPk } = action.payload;
if (infos && members) { if (infos && members) {
@ -1437,8 +1371,6 @@ const metaGroupSlice = createSlice({
export const groupInfoActions = { export const groupInfoActions = {
initNewGroupInWrapper, initNewGroupInWrapper,
loadMetaDumpsFromDB, loadMetaDumpsFromDB,
destroyGroupDetails,
emptyGroupButKeepAsKicked,
refreshGroupDetailsFromWrapper, refreshGroupDetailsFromWrapper,
handleUserGroupUpdate, handleUserGroupUpdate,
currentDeviceGroupMembersChange, currentDeviceGroupMembersChange,

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

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

@ -268,61 +268,6 @@ export function getLegacyGroupInfoFromDBValues({
return legacyGroup; 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. * 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