From 2068737cdd2f291060233bfc96399aef7f005be8 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 26 May 2023 15:51:23 +1000 Subject: [PATCH] fix: break down deleteContact based on convo type --- package.json | 2 +- preload.js | 3 +- .../dialog/AdminLeaveClosedGroupDialog.tsx | 3 +- ts/components/menu/Menu.tsx | 17 +- ts/interactions/conversationInteractions.ts | 18 +- ts/receiver/closedGroups.ts | 143 +++------- ts/receiver/configMessage.ts | 25 +- .../opengroupV2/JoinOpenGroupV2.ts | 2 +- .../opengroupV2/OpenGroupManagerV2.ts | 2 +- .../open_group_api/utils/OpenGroupUtils.ts | 2 +- ts/session/apis/snode_api/swarmPolling.ts | 6 +- .../conversations/ConversationController.ts | 268 ++++++++++-------- ts/session/sending/MessageSender.ts | 1 + ts/session/utils/AttachmentsDownload.ts | 2 +- .../job_runners/jobs/ConfigurationSyncJob.ts | 4 +- ts/util/logging.ts | 2 +- ts/util/privacy.ts | 8 +- ts/window.d.ts | 3 +- yarn.lock | 6 +- 19 files changed, 250 insertions(+), 267 deletions(-) diff --git a/package.json b/package.json index d42c4575b..9f166bea7 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "glob": "7.1.2", "image-type": "^4.1.0", "ip2country": "1.0.1", - "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz", + "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.16/libsession_util_nodejs-v0.1.16.tar.gz", "libsodium-wrappers-sumo": "^0.7.9", "linkify-it": "3.0.2", "lodash": "^4.17.21", diff --git a/preload.js b/preload.js index fe8c08818..37d5765e0 100644 --- a/preload.js +++ b/preload.js @@ -31,9 +31,10 @@ window.sessionFeatureFlags = { useTestNet: Boolean( process.env.NODE_APP_INSTANCE && process.env.NODE_APP_INSTANCE.includes('testnet') ), - useDebugLogging: !_.isEmpty(process.env.SESSION_DEBUG), useClosedGroupV3: false || process.env.USE_CLOSED_GROUP_V3, debug: { + debugLogging: !_.isEmpty(process.env.SESSION_DEBUG), + debugLibsessionDumps: !_.isEmpty(process.env.SESSION_DEBUG_LIBSESSION_DUMPS), debugFileServerRequests: false, debugNonSnodeRequests: false, debugOnionRequests: false, diff --git a/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx b/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx index 567322b90..d5e7a6f9b 100644 --- a/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx +++ b/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx @@ -29,8 +29,9 @@ export const AdminLeaveClosedGroupDialog = (props: { conversationId: string }) = } setLoading(true); // we know want to delete a closed group right after we've left it, so we can call the deleteContact which takes care of it all - await getConversationController().deleteContact(props.conversationId, { + await getConversationController().deleteClosedGroup(props.conversationId, { fromSyncMessage: false, + sendLeaveMessage: true, }); setLoading(false); closeDialog(); diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index 5617252ca..f9e277815 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -112,7 +112,7 @@ export const DeletePrivateContactMenuItem = () => { onClickClose, okTheme: SessionButtonColor.Danger, onClickOk: async () => { - await getConversationController().deleteContact(convoId, { + await getConversationController().delete1o1(convoId, { fromSyncMessage: false, justHidePrivate: false, }); @@ -152,9 +152,16 @@ export const DeleteGroupOrCommunityMenuItem = () => { onClickClose, okTheme: SessionButtonColor.Danger, onClickOk: async () => { - await getConversationController().deleteContact(convoId, { - fromSyncMessage: false, - }); + if (isPublic) { + await getConversationController().deleteCommunity(convoId, { + fromSyncMessage: false, + }); + } else { + await getConversationController().deleteClosedGroup(convoId, { + fromSyncMessage: false, + sendLeaveMessage: true, + }); + } }, }) ); @@ -450,7 +457,7 @@ export const DeletePrivateConversationMenuItem = () => { return ( { - await getConversationController().deleteContact(convoId, { + await getConversationController().delete1o1(convoId, { fromSyncMessage: false, justHidePrivate: true, }); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index eb2b2da3c..6b6c7ca27 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -238,9 +238,10 @@ export function showLeaveGroupByConvoId(conversationId: string) { UserUtils.getOurPubKeyStrFromCache() ); const isClosedGroup = conversation.isClosedGroup() || false; + const isPublic = conversation.isPublic() || false; - // if this is not a closed group, or we are not admin, we can just show a confirmation dialog - if (!isClosedGroup || (isClosedGroup && !isAdmin)) { + // if this is a community, or we legacy group are not admin, we can just show a confirmation dialog + if (isPublic || (isClosedGroup && !isAdmin)) { const onClickClose = () => { window.inboxStore?.dispatch(updateConfirmModal(null)); }; @@ -249,9 +250,16 @@ export function showLeaveGroupByConvoId(conversationId: string) { title, message, onClickOk: async () => { - await getConversationController().deleteContact(conversation.id, { - fromSyncMessage: false, - }); + if (isPublic) { + await getConversationController().deleteCommunity(conversation.id, { + fromSyncMessage: false, + }); + } else { + await getConversationController().deleteClosedGroup(conversation.id, { + fromSyncMessage: false, + sendLeaveMessage: true, + }); + } onClickClose(); }, onClickClose, diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index b7393741c..9b99a76b3 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -64,7 +64,7 @@ export async function addKeyPairToCacheAndDBIfNeeded( return true; } -export async function innerRemoveAllClosedGroupEncryptionKeyPairs(groupPubKey: string) { +export async function removeAllClosedGroupEncryptionKeyPairs(groupPubKey: string) { cacheOfClosedGroupKeyPairs.set(groupPubKey, []); await Data.removeAllClosedGroupEncryptionKeyPairs(groupPubKey); } @@ -327,26 +327,6 @@ export async function handleNewClosedGroup( await queueAllCachedFromSource(groupId); } -/** - * - * @param isKicked if true, we mark the reason for leaving as a we got kicked - */ -export async function markGroupAsLeftOrKicked( - groupPublicKey: string, - groupConvo: ConversationModel, - isKicked: boolean -) { - getSwarmPollingInstance().removePubkey(groupPublicKey); - - await innerRemoveAllClosedGroupEncryptionKeyPairs(groupPublicKey); - - if (isKicked) { - groupConvo.set('isKickedFromGroup', true); - } else { - groupConvo.set('left', true); - } -} - /** * This function is called when we get a message with the new encryption keypair for a closed group. * In this message, we have n-times the same keypair encoded with n being the number of current members. @@ -681,34 +661,39 @@ async function handleClosedGroupMembersRemoved( // • Stop polling for the group // • Remove the key pairs associated with the group const ourPubKey = UserUtils.getOurPubKeyFromCache(); - const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); - if (wasCurrentUserRemoved) { - await markGroupAsLeftOrKicked(groupPubKey, convo, true); - } - // Note: we don't want to send a new encryption keypair when we get a member removed. - // this is only happening when the admin gets a MEMBER_LEFT message - - // Only add update message if we have something to show - if (membersAfterUpdate.length !== currentMembers.length) { - const groupDiff: ClosedGroup.GroupDiff = { - kickedMembers: effectivelyRemovedMembers, - }; - await ClosedGroup.addUpdateMessage( - convo, - groupDiff, - envelope.senderIdentity, - _.toNumber(envelope.timestamp) - ); - convo.updateLastMessage(); - } + const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key); + if (wasCurrentUserKicked) { + // we now want to remove everything related to a group when we get kicked from it. + await getConversationController().deleteClosedGroup(groupPubKey, { + fromSyncMessage: false, + sendLeaveMessage: false, + }); + } else { + // Note: we don't want to send a new encryption keypair when we get a member removed. + // this is only happening when the admin gets a MEMBER_LEFT message + + // Only add update message if we have something to show + if (membersAfterUpdate.length !== currentMembers.length) { + const groupDiff: ClosedGroup.GroupDiff = { + kickedMembers: effectivelyRemovedMembers, + }; + await ClosedGroup.addUpdateMessage( + convo, + groupDiff, + envelope.senderIdentity, + _.toNumber(envelope.timestamp) + ); + convo.updateLastMessage(); + } - // Update the group - const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z)); + // Update the group + const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z)); - convo.set({ members: membersAfterUpdate }); - convo.set({ zombies }); + convo.set({ members: membersAfterUpdate }); + convo.set({ zombies }); - await convo.commit(); + await convo.commit(); + } await removeFromCache(envelope); } @@ -758,56 +743,21 @@ function removeMemberFromZombies( return true; } -async function handleClosedGroupAdminMemberLeft( - groupPublicKey: string, - isCurrentUserAdmin: boolean, - convo: ConversationModel, - envelope: EnvelopePlus -) { +async function handleClosedGroupAdminMemberLeft(groupPublicKey: string, envelope: EnvelopePlus) { // if the admin was remove and we are the admin, it can only be voluntary - await markGroupAsLeftOrKicked(groupPublicKey, convo, !isCurrentUserAdmin); - - // everybody left ! this is how we disable a group when the admin left - const groupDiff: ClosedGroup.GroupDiff = { - kickedMembers: convo.get('members'), - }; - convo.set('members', []); - - await ClosedGroup.addUpdateMessage( - convo, - groupDiff, - envelope.senderIdentity, - _.toNumber(envelope.timestamp) - ); - convo.updateLastMessage(); - - await convo.commit(); + await getConversationController().deleteClosedGroup(groupPublicKey, { + fromSyncMessage: false, + sendLeaveMessage: false, + }); await removeFromCache(envelope); } -async function handleClosedGroupLeftOurself( - groupPublicKey: string, - convo: ConversationModel, - envelope: EnvelopePlus -) { - await markGroupAsLeftOrKicked(groupPublicKey, convo, false); - const groupDiff: ClosedGroup.GroupDiff = { - leavingMembers: [envelope.senderIdentity], - }; - await ClosedGroup.addUpdateMessage( - convo, - groupDiff, - envelope.senderIdentity, - _.toNumber(envelope.timestamp) - ); - convo.updateLastMessage(); - // remove ourself from the list of members - convo.set( - 'members', - convo.get('members').filter(m => !UserUtils.isUsFromCache(m)) - ); - - await convo.commit(); +async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopePlus) { + // 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 getConversationController().deleteClosedGroup(groupId, { + fromSyncMessage: false, + sendLeaveMessage: false, + }); await removeFromCache(envelope); } @@ -827,18 +777,17 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver } const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); - // if the admin leaves, the group is disabled for every members - const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourPubkey) || false; + // if the admin leaves, the group is disabled for everyone if (didAdminLeave) { - await handleClosedGroupAdminMemberLeft(groupPublicKey, isCurrentUserAdmin, convo, envelope); + await handleClosedGroupAdminMemberLeft(groupPublicKey, envelope); return; } // if we are no longer a member, we LEFT from another device if (!newMembers.includes(ourPubkey)) { - // stop polling, remove all stored pubkeys and make sure the UI does not let us write messages - await handleClosedGroupLeftOurself(groupPublicKey, convo, envelope); + // stop polling, remove everything from this closed group and the corresponding conversation + await handleClosedGroupLeftOurself(groupPublicKey, envelope); return; } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 3b250b0c3..b4f2ee228 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -53,8 +53,6 @@ import { queueAllCachedFromSource } from './receiver'; import { EnvelopePlus } from './types'; import { deleteAllMessagesByConvoIdNoConfirmation } from '../interactions/conversationInteractions'; -const printDumpsForDebugging = false; - function groupByVariant( incomingConfigs: Array> ) { @@ -99,7 +97,7 @@ async function mergeConfigsWithIncomingUpdates( data: msg.message.data, hash: msg.messageHash, })); - if (printDumpsForDebugging) { + if (window.sessionFeatureFlags.debug.debugLibsessionDumps) { window.log.info( `printDumpsForDebugging: before merge of ${variant}:`, StringUtils.toHex(await GenericWrapperActions.dump(variant)) @@ -125,7 +123,7 @@ async function mergeConfigsWithIncomingUpdates( `${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} ` ); - if (printDumpsForDebugging) { + if (window.sessionFeatureFlags.debug.debugLibsessionDumps) { window.log.info( `printDumpsForDebugging: after merge of ${variant}:`, StringUtils.toHex(await GenericWrapperActions.dump(variant)) @@ -213,7 +211,7 @@ async function deleteContactsFromDB(contactsToRemove: Array) { for (let index = 0; index < contactsToRemove.length; index++) { const contactToRemove = contactsToRemove[index]; try { - await getConversationController().deleteContact(contactToRemove, { + await getConversationController().delete1o1(contactToRemove, { fromSyncMessage: true, justHidePrivate: false, }); @@ -360,7 +358,7 @@ async function handleCommunitiesUpdate() { for (let index = 0; index < communitiesToLeaveInDB.length; index++) { const toLeave = communitiesToLeaveInDB[index]; window.log.info('leaving community with convoId ', toLeave.id); - await getConversationController().deleteContact(toLeave.id, { + await getConversationController().deleteCommunity(toLeave.id, { fromSyncMessage: true, }); } @@ -442,16 +440,11 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { toLeave.id ); const toLeaveFromDb = getConversationController().get(toLeave.id); - - // if we were kicked from that group, leave it as is until the user manually deletes it - // otherwise, completely remove the conversation - if (!toLeaveFromDb?.get('isKickedFromGroup')) { - window.log.debug(`we were kicked from ${toLeave.id} so we keep it until manually deleted`); - - await getConversationController().deleteContact(toLeave.id, { - fromSyncMessage: true, - }); - } + // 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 getConversationController().deleteClosedGroup(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. + }); } for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) { diff --git a/ts/session/apis/open_group_api/opengroupV2/JoinOpenGroupV2.ts b/ts/session/apis/open_group_api/opengroupV2/JoinOpenGroupV2.ts index e2ad61955..b7b4bd62f 100644 --- a/ts/session/apis/open_group_api/opengroupV2/JoinOpenGroupV2.ts +++ b/ts/session/apis/open_group_api/opengroupV2/JoinOpenGroupV2.ts @@ -82,7 +82,7 @@ async function joinOpenGroupV2( // we already have a convo associated with it. Remove everything related to it so we start fresh window?.log?.warn('leaving before rejoining open group v2 room', conversationId); - await getConversationController().deleteContact(conversationId, { + await getConversationController().deleteCommunity(conversationId, { fromSyncMessage: true, }); } diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts index 868e0bbb8..b87264de5 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts @@ -152,7 +152,7 @@ export class OpenGroupManagerV2 { await OpenGroupData.removeV2OpenGroupRoom(roomConvoId); getOpenGroupManager().removeRoomFromPolledRooms(infos); - await getConversationController().deleteContact(roomConvoId, { + await getConversationController().deleteCommunity(roomConvoId, { fromSyncMessage: false, }); } diff --git a/ts/session/apis/open_group_api/utils/OpenGroupUtils.ts b/ts/session/apis/open_group_api/utils/OpenGroupUtils.ts index 9fecfeea0..d7b579f24 100644 --- a/ts/session/apis/open_group_api/utils/OpenGroupUtils.ts +++ b/ts/session/apis/open_group_api/utils/OpenGroupUtils.ts @@ -112,5 +112,5 @@ export function getOpenGroupV2FromConversationId( * Check if this conversation id corresponds to an OpenGroupV2 conversation. */ export function isOpenGroupV2(conversationId: string) { - return Boolean(conversationId.startsWith(openGroupPrefix)); + return Boolean(conversationId?.startsWith(openGroupPrefix)); } diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 88da1ef0e..6797d7178 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -109,8 +109,10 @@ export class SwarmPolling { public removePubkey(pk: PubKey | string) { const pubkey = PubKey.cast(pk); - window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); - this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey)); + if (this.groupPolling.some(group => pubkey.key === group.pubkey.key)) { + window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); + this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey)); + } } /** diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 42c09414b..0dddc0e1a 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -22,13 +22,14 @@ import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_uti import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups'; import { GetNetworkTime } from '../apis/snode_api/getNetworkTime'; import { getMessageQueue } from '..'; -import { markGroupAsLeftOrKicked } from '../../receiver/closedGroups'; import { getSwarmPollingInstance } from '../apis/snode_api'; import { SnodeNamespaces } from '../apis/snode_api/namespaces'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { UserUtils } from '../utils'; import { isEmpty, isNil } from 'lodash'; import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations'; +import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; +import { OpenGroupUtils } from '../apis/open_group_api/utils'; let instance: ConversationController | null; @@ -204,114 +205,88 @@ export class ConversationController { await conversation.commit(); } - public async deleteContact( - id: string, - options: { fromSyncMessage: boolean; justHidePrivate?: boolean } + public async deleteClosedGroup( + groupId: string, + options: { fromSyncMessage: boolean; sendLeaveMessage: boolean } ) { - if (!this._initialFetchComplete) { - throw new Error('getConversationController.deleteContact needs complete initial fetch'); + const conversation = await this.deleteConvoInitialChecks(groupId, 'LegacyGroup'); + if (!conversation || !conversation.isClosedGroup()) { + return; } + window.log.info(`deleteClosedGroup: ${groupId}, sendLeaveMessage?:${options.sendLeaveMessage}`); + getSwarmPollingInstance().removePubkey(groupId); // we don't need to keep polling anymore. - window.log.info(`deleteContact with ${id}`); + if (options.sendLeaveMessage) { + await leaveClosedGroup(groupId, options.fromSyncMessage); + } - const conversation = this.conversations.get(id); - if (!conversation) { - window.log.warn(`deleteContact no such convo ${id}`); - return; + // 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. + await this.removeGroupOrCommunityFromDBAndRedux(groupId); + await removeLegacyGroupFromWrappers(groupId); + + if (!options.fromSyncMessage) { + await ConfigurationSync.queueNewJobIfNeeded(); } + } - // those are the stuff to do for all conversation types - window.log.info(`deleteContact destroyingMessages: ${id}`); - await deleteAllMessagesByConvoIdNoConfirmation(id); - window.log.info(`deleteContact messages destroyed: ${id}`); - - const convoType: ConvoVolatileType = conversation.isClosedGroup() - ? 'LegacyGroup' - : conversation.isPublic() - ? 'Community' - : '1o1'; - - switch (convoType) { - case '1o1': - // if this conversation is a private conversation it's in fact a `contact` for desktop. - - if (options.justHidePrivate || isNil(options.justHidePrivate) || conversation.isMe()) { - // we just set the hidden field to true - // so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation. - // We also keep the messages for now, as turning a contact as hidden might just be a temporary thing - window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`); - conversation.set({ - priority: CONVERSATION_PRIORITIES.hidden, - }); - // We don't remove entries from the contacts wrapper, so better keep corresponding convo volatile info for now (it will be pruned if needed) - await conversation.commit(); // this updates the wrappers content to reflect the hidden state - } else { - window.log.info(`deleteContact isPrivate, reset fields and removing from wrapper: ${id}`); - - await conversation.setIsApproved(false, false); - await conversation.setDidApproveMe(false, false); - conversation.set('active_at', 0); - await BlockedNumberController.unblockAll([conversation.id]); - await conversation.commit(); // first commit to DB so the DB knows about the changes - if (SessionUtilContact.isContactToStoreInWrapper(conversation)) { - window.log.warn('isContactToStoreInWrapper still true for ', conversation.attributes); - } - if (conversation.id.startsWith('05')) { - // make sure to filter blinded contacts as it will throw otherwise - await SessionUtilContact.removeContactFromWrapper(conversation.id); // then remove the entry alltogether from the wrapper - await SessionUtilConvoInfoVolatile.removeContactFromWrapper(conversation.id); - } - if (getCurrentlySelectedConversationOutsideRedux() === conversation.id) { - window.inboxStore?.dispatch(resetConversationExternal()); - } - } + public async deleteCommunity(convoId: string, options: { fromSyncMessage: boolean }) { + const conversation = await this.deleteConvoInitialChecks(convoId, 'Community'); + if (!conversation || !conversation.isPublic()) { + return; + } - break; - case 'Community': - window?.log?.info('leaving open group v2', conversation.id); - - try { - const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(conversation.id); - if (fromWrapper?.fullUrlWithPubkey) { - await SessionUtilConvoInfoVolatile.removeCommunityFromWrapper( - conversation.id, - fromWrapper.fullUrlWithPubkey - ); - } - } catch (e) { - window?.log?.info( - 'SessionUtilConvoInfoVolatile.removeCommunityFromWrapper failed:', - e.message - ); - } + window?.log?.info('leaving community: ', conversation.id); + const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversation.id); + if (roomInfos) { + getOpenGroupManager().removeRoomFromPolledRooms(roomInfos); + } + await removeCommunityFromWrappers(conversation.id); // this call needs to fetch the pubkey + await this.removeGroupOrCommunityFromDBAndRedux(conversation.id); - // remove from the wrapper the entries before we remove the roomInfos, as we won't have the required community pubkey afterwards - try { - await SessionUtilUserGroups.removeCommunityFromWrapper(conversation.id, conversation.id); - } catch (e) { - window?.log?.info('SessionUtilUserGroups.removeCommunityFromWrapper failed:', e); - } + if (!options.fromSyncMessage) { + await ConfigurationSync.queueNewJobIfNeeded(); + } + } - const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversation.id); - if (roomInfos) { - getOpenGroupManager().removeRoomFromPolledRooms(roomInfos); - } - await this.cleanUpGroupConversation(conversation.id); + public async delete1o1( + id: string, + options: { fromSyncMessage: boolean; justHidePrivate?: boolean } + ) { + const conversation = await this.deleteConvoInitialChecks(id, '1o1'); + if (!conversation || !conversation.isPrivate()) { + return; + } - // remove the roomInfos locally for this open group room including the pubkey - try { - await OpenGroupData.removeV2OpenGroupRoom(conversation.id); - } catch (e) { - window?.log?.info('removeV2OpenGroupRoom failed:', e); - } - break; - case 'LegacyGroup': - window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`); - await leaveClosedGroup(conversation.id, options.fromSyncMessage); // this removes the data from the group and convo volatile info - await this.cleanUpGroupConversation(conversation.id); - break; - default: - assertUnreachable(convoType, `deleteContact: convoType ${convoType} not handled`); + if (options.justHidePrivate || isNil(options.justHidePrivate) || conversation.isMe()) { + // we just set the hidden field to true + // so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation. + // We also keep the messages for now, as turning a contact as hidden might just be a temporary thing + window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`); + conversation.set({ + priority: CONVERSATION_PRIORITIES.hidden, + }); + // We don't remove entries from the contacts wrapper, so better keep corresponding convo volatile info for now (it will be pruned if needed) + await conversation.commit(); // this updates the wrappers content to reflect the hidden state + } else { + window.log.info(`deleteContact isPrivate, reset fields and removing from wrapper: ${id}`); + + await conversation.setIsApproved(false, false); + await conversation.setDidApproveMe(false, false); + conversation.set('active_at', 0); + await BlockedNumberController.unblockAll([conversation.id]); + await conversation.commit(); // first commit to DB so the DB knows about the changes + if (SessionUtilContact.isContactToStoreInWrapper(conversation)) { + window.log.warn('isContactToStoreInWrapper still true for ', conversation.attributes); + } + if (conversation.id.startsWith('05')) { + // make sure to filter blinded contacts as it will throw otherwise + await SessionUtilContact.removeContactFromWrapper(conversation.id); // then remove the entry alltogether from the wrapper + await SessionUtilConvoInfoVolatile.removeContactFromWrapper(conversation.id); + } + if (getCurrentlySelectedConversationOutsideRedux() === conversation.id) { + window.inboxStore?.dispatch(resetConversationExternal()); + } } if (!options.fromSyncMessage) { @@ -415,32 +390,62 @@ export class ConversationController { this.conversations.reset([]); } - private async cleanUpGroupConversation(id: string) { - window.log.info(`deleteContact isGroup, removing convo from DB: ${id}`); + private async deleteConvoInitialChecks(convoId: string, deleteType: ConvoVolatileType) { + if (!this._initialFetchComplete) { + throw new Error(`getConversationController.${deleteType} needs complete initial fetch`); + } + + window.log.info(`${deleteType} with ${convoId}`); + + const conversation = this.conversations.get(convoId); + if (!conversation) { + window.log.warn(`${deleteType} no such convo ${convoId}`); + return null; + } + + // those are the stuff to do for all conversation types + window.log.info(`${deleteType} destroyingMessages: ${convoId}`); + await deleteAllMessagesByConvoIdNoConfirmation(convoId); + window.log.info(`${deleteType} messages destroyed: ${convoId}`); + return conversation; + } + + private async removeGroupOrCommunityFromDBAndRedux(convoId: string) { + window.log.info(`cleanUpGroupConversation, removing convo from DB: ${convoId}`); // not a private conversation, so not a contact for the ContactWrapper - await Data.removeConversation(id); + await Data.removeConversation(convoId); - window.log.info(`deleteContact isGroup, convo removed from DB: ${id}`); - const conversation = this.conversations.get(id); + // remove the data from the opengrouprooms table too if needed + if (convoId && OpenGroupUtils.isOpenGroupV2(convoId)) { + // remove the roomInfos locally for this open group room including the pubkey + try { + await OpenGroupData.removeV2OpenGroupRoom(convoId); + } catch (e) { + window?.log?.info('removeV2OpenGroupRoom failed:', e); + } + } + + window.log.info(`cleanUpGroupConversation, convo removed from DB: ${convoId}`); + const conversation = this.conversations.get(convoId); if (conversation) { this.conversations.remove(conversation); window?.inboxStore?.dispatch( conversationActions.conversationChanged({ - id: id, + id: convoId, data: conversation.getConversationModelProps(), }) ); } - window.inboxStore?.dispatch(conversationActions.conversationRemoved(id)); + window.inboxStore?.dispatch(conversationActions.conversationRemoved(convoId)); - window.log.info(`deleteContact NOT private, convo removed from store: ${id}`); + window.log.info(`cleanUpGroupConversation, convo removed from store: ${convoId}`); } } /** - * You most likely don't want to call this function directly, but instead use the deleteContact() from the ConversationController as it will take care of more cleaningup. + * You most likely don't want to call this function directly, but instead use the deleteLegacyGroup() from the ConversationController as it will take care of more cleaningup. * * Note: `fromSyncMessage` is used to know if we need to send a leave group message to the group first. * So if the user made the action on this device, fromSyncMessage should be false, but if it happened from a linked device polled update, set this to true. @@ -475,20 +480,12 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { await convo.updateGroupAdmins(admins, false); await convo.commit(); - const source = UserUtils.getOurPubKeyStrFromCache(); const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset(); - const dbMessage = await convo.addSingleOutgoingMessage({ - group_update: { left: [source] }, - sent_at: networkTimestamp, - expireTimer: 0, - }); - getSwarmPollingInstance().removePubkey(groupId); if (fromSyncMessage) { // no need to send our leave message as our other device should already have sent it. - await cleanUpFullyLeftLegacyGroup(groupId); return; } @@ -496,7 +493,6 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { if (!keypair || isEmpty(keypair) || isEmpty(keypair.publicHex) || isEmpty(keypair.privateHex)) { // if we do not have a keypair, we won't be able to send our leaving message neither, so just skip sending it. // this can happen when getting a group from a broken libsession usergroup wrapper, but not only. - await cleanUpFullyLeftLegacyGroup(groupId); return; } @@ -504,7 +500,6 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ timestamp: networkTimestamp, groupId, - identifier: dbMessage.id as string, }); window?.log?.info(`We are leaving the group ${groupId}. Sending our leaving message.`); @@ -521,17 +516,42 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { window?.log?.info( `Leaving message sent ${groupId}. Removing everything related to this group.` ); - await cleanUpFullyLeftLegacyGroup(groupId); + } else { + window?.log?.info( + `Leaving message failed to be sent for ${groupId}. But still removing everything related to this group....` + ); } - // if we failed to send our leaving message, don't remove everything yet as we might want to retry sending our leaving message later. + // the rest of the cleaning of that conversation is done in the `deleteClosedGroup()` } -async function cleanUpFullyLeftLegacyGroup(groupId: string) { - const convo = getConversationController().get(groupId); +async function removeLegacyGroupFromWrappers(groupId: string) { + getSwarmPollingInstance().removePubkey(groupId); await UserGroupsWrapperActions.eraseLegacyGroup(groupId); await SessionUtilConvoInfoVolatile.removeLegacyGroupFromWrapper(groupId); - if (convo) { - await markGroupAsLeftOrKicked(groupId, convo, false); + await removeAllClosedGroupEncryptionKeyPairs(groupId); +} + +async function removeCommunityFromWrappers(conversationId: string) { + if (!conversationId || !OpenGroupUtils.isOpenGroupV2(conversationId)) { + return; + } + try { + const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(conversationId); + if (fromWrapper?.fullUrlWithPubkey) { + await SessionUtilConvoInfoVolatile.removeCommunityFromWrapper( + conversationId, + fromWrapper.fullUrlWithPubkey + ); + } + } catch (e) { + window?.log?.info('SessionUtilConvoInfoVolatile.removeCommunityFromWrapper failed:', e.message); + } + + // remove from the wrapper the entries before we remove the roomInfos, as we won't have the required community pubkey afterwards + try { + await SessionUtilUserGroups.removeCommunityFromWrapper(conversationId, conversationId); + } catch (e) { + window?.log?.info('SessionUtilUserGroups.removeCommunityFromWrapper failed:', e.message); } } diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index f9b231244..33612d56a 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -404,6 +404,7 @@ async function sendMessagesToSnode( retries: 2, factor: 1, minTimeout: MessageSender.getMinRetryTimeout(), + maxTimeout: 1000, } ); diff --git a/ts/session/utils/AttachmentsDownload.ts b/ts/session/utils/AttachmentsDownload.ts index 51b81728f..9838cedb3 100644 --- a/ts/session/utils/AttachmentsDownload.ts +++ b/ts/session/utils/AttachmentsDownload.ts @@ -238,7 +238,7 @@ async function _runJob(job: any) { if (currentAttempt >= 3 || was404Error(error)) { logger.error( `_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${found?.idForLogging()} as permanent error:`, - error && error.stack ? error.stack : error + error && error.message ? error.message : error ); // Make sure to fetch the message from DB here right before writing it. diff --git a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts index 4513c5cf9..587169481 100644 --- a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts @@ -19,8 +19,8 @@ import { import { ReleasedFeatures } from '../../../../util/releaseFeature'; import { allowOnlyOneAtATime } from '../../Promise'; -const defaultMsBetweenRetries = 3000; -const defaultMaxAttempts = 3; +const defaultMsBetweenRetries = 30000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s) +const defaultMaxAttempts = 2; /** * We want to run each of those jobs at least 3seconds apart. diff --git a/ts/util/logging.ts b/ts/util/logging.ts index 25269d710..9c7a6d9bf 100644 --- a/ts/util/logging.ts +++ b/ts/util/logging.ts @@ -110,7 +110,7 @@ const development = window && window?.getEnvironment && window?.getEnvironment() // The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api function logAtLevel(level: string, prefix: string, ...args: any) { - if (prefix === 'DEBUG' && !window.sessionFeatureFlags.useDebugLogging) { + if (prefix === 'DEBUG' && !window.sessionFeatureFlags.debug.debugLogging) { return; } if (development) { diff --git a/ts/util/privacy.ts b/ts/util/privacy.ts index 2ab8a5e36..203e08498 100644 --- a/ts/util/privacy.ts +++ b/ts/util/privacy.ts @@ -1,8 +1,8 @@ /* eslint-env node */ // tslint:disable-next-line: no-submodule-imports +import { escapeRegExp, isEmpty, isRegExp, isString } from 'lodash'; import { compose } from 'lodash/fp'; -import { escapeRegExp, isNil, isRegExp, isString } from 'lodash'; import { getAppRootPath } from '../node/getRootPath'; const APP_ROOT_PATH = getAppRootPath(); @@ -99,9 +99,9 @@ const removeNewlines = (text: string) => text.replace(/\r?\n|\r/g, ''); const redactSensitivePaths = redactPath(APP_ROOT_PATH); function shouldNotRedactLogs() { - // if the env variable `SESSION_NO_REDACT` is set, trust it as a boolean - if (!isNil(process.env.SESSION_NO_REDACT)) { - return process.env.SESSION_NO_REDACT; + // if featureFlag is set to true, trust it + if (!isEmpty(process.env.SESSION_DEBUG_DISABLE_REDACTED)) { + return true; } // otherwise we don't want to redact logs when running on the devprod env return (process.env.NODE_APP_INSTANCE || '').startsWith('devprod'); diff --git a/ts/window.d.ts b/ts/window.d.ts index 586a787ed..2d467ccc3 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -38,11 +38,12 @@ declare global { useTestNet: boolean; useClosedGroupV3: boolean; debug: { + debugLogging: boolean; + debugLibsessionDumps: boolean; debugFileServerRequests: boolean; debugNonSnodeRequests: boolean; debugOnionRequests: boolean; }; - useDebugLogging: boolean; }; SessionSnodeAPI: SessionSnodeAPI; onLogin: (pw: string) => Promise; diff --git a/yarn.lock b/yarn.lock index 8c4660270..09062071d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5148,9 +5148,9 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz": - version "0.1.15" - resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz#276b878bbd68261009dd1081b97e25ee6769fd62" +"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.16/libsession_util_nodejs-v0.1.16.tar.gz": + version "0.1.16" + resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.16/libsession_util_nodejs-v0.1.16.tar.gz#2a526154b7d0f4235895f3a788704a56d6573339" dependencies: cmake-js "^7.2.1" node-addon-api "^6.1.0"