From 6f6620f622b1f24235e17e724766b96e6d83fb15 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 8 May 2023 14:56:21 +1000 Subject: [PATCH] fix: skip new group messages when receved after group was removed --- .../dialog/AdminLeaveClosedGroupDialog.tsx | 43 +++--- ts/interactions/conversationInteractions.ts | 29 ++-- ts/models/message.ts | 4 +- ts/node/migration/sessionMigrations.ts | 30 ++-- ts/receiver/closedGroups.ts | 9 +- ts/receiver/configMessage.ts | 68 +++++---- ts/receiver/receiver.ts | 20 +-- ts/session/apis/snode_api/swarmPolling.ts | 44 ++++-- .../conversations/ConversationController.ts | 140 +++++++++++++++--- ts/session/conversations/createClosedGroup.ts | 17 +-- ts/session/crypto/MessageEncrypter.ts | 1 + ts/session/group/closed-group.ts | 88 ++--------- ts/session/sending/MessageQueue.ts | 6 +- .../libsession_utils_user_groups.ts | 12 +- ts/state/ducks/conversations.ts | 4 +- ts/util/privacy.ts | 13 +- 16 files changed, 301 insertions(+), 227 deletions(-) diff --git a/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx b/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx index 36c433593..c42cd999f 100644 --- a/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx +++ b/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx @@ -1,51 +1,54 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { getConversationController } from '../../session/conversations'; -import { leaveClosedGroup } from '../../session/group/closed-group'; import { adminLeaveClosedGroup } from '../../state/ducks/modalDialog'; +import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SpacerLG } from '../basic/Text'; -import { SessionWrapperModal } from '../SessionWrapperModal'; - -type Props = { - conversationId: string; -}; +import { SessionSpinner } from '../basic/SessionSpinner'; const StyledWarning = styled.p` max-width: 500px; line-height: 1.3333; `; -export const AdminLeaveClosedGroupDialog = (props: Props) => { +export const AdminLeaveClosedGroupDialog = (props: { conversationId: string }) => { + const dispatch = useDispatch(); const convo = getConversationController().get(props.conversationId); - const titleText = `${window.i18n('leaveGroup')} ${convo.getRealSessionUsername()}`; - const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`; - const okText = window.i18n('leaveAndRemoveForEveryone'); - const cancelText = window.i18n('cancel'); + const [loading, setLoading] = useState(false); + const titleText = `${window.i18n('leaveGroup')} ${convo?.getRealSessionUsername() || ''}`; - const onClickOK = async () => { - await leaveClosedGroup(props.conversationId); - closeDialog(); + const closeDialog = () => { + dispatch(adminLeaveClosedGroup(null)); }; - const closeDialog = () => { - window.inboxStore?.dispatch(adminLeaveClosedGroup(null)); + const onClickOK = async () => { + if (loading) { + return; + } + 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, false); + setLoading(false); + closeDialog(); }; return ( - {warningAsAdmin} + {window.i18n('leaveGroupConfirmationAdmin')} +
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 77ca33c4d..a2f7ec463 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -7,13 +7,16 @@ import { CallManager, SyncUtils, ToastUtils, UserUtils } from '../session/utils' import { SessionButtonColor } from '../components/basic/SessionButton'; import { getCallMediaPermissionsSettings } from '../components/settings/SessionSettings'; import { Data } from '../data/data'; +import { SettingsKey } from '../data/settings-key'; import { uploadFileToFsWithOnionV4 } from '../session/apis/file_server_api/FileServerApi'; +import { OpenGroupUtils } from '../session/apis/open_group_api/utils'; import { getConversationController } from '../session/conversations'; import { getSodiumRenderer } from '../session/crypto'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; -import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob'; import { perfEnd, perfStart } from '../session/utils/Performance'; import { fromHexToArray, toHex } from '../session/utils/String'; +import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob'; +import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts'; import { forceSyncConfigurationNowIfNeeded } from '../session/utils/sync/syncUtils'; import { conversationReset, @@ -32,17 +35,13 @@ import { updateRemoveModeratorsModal, } from '../state/ducks/modalDialog'; import { MIME } from '../types'; -import { urlToBlob } from '../types/attachments/VisualAttachment'; -import { processNewAttachment } from '../types/MessageAttachment'; import { IMAGE_JPEG } from '../types/MIME'; +import { processNewAttachment } from '../types/MessageAttachment'; +import { urlToBlob } from '../types/attachments/VisualAttachment'; import { BlockedNumberController } from '../util/blockedNumberController'; import { encryptProfile } from '../util/crypto/profileEncrypter'; -import { Storage, setLastProfileUpdateTimestamp } from '../util/storage'; -import { OpenGroupUtils } from '../session/apis/open_group_api/utils'; -import { leaveClosedGroup } from '../session/group/closed-group'; -import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts'; -import { SettingsKey } from '../data/settings-key'; import { ReleasedFeatures } from '../util/releaseFeature'; +import { Storage, setLastProfileUpdateTimestamp } from '../util/storage'; import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface'; export async function copyPublicKeyByConvoId(convoId: string) { @@ -250,19 +249,19 @@ export function showLeaveGroupByConvoId(conversationId: string) { title, message, onClickOk: async () => { - await leaveClosedGroup(conversation.id); + await getConversationController().deleteContact(conversation.id, false); onClickClose(); }, onClickClose, }) ); - } else { - window.inboxStore?.dispatch( - adminLeaveClosedGroup({ - conversationId, - }) - ); + return; } + window.inboxStore?.dispatch( + adminLeaveClosedGroup({ + conversationId, + }) + ); } export function showInviteContactByConvoId(conversationId: string) { window.inboxStore?.dispatch(updateInviteContactModal({ conversationId })); diff --git a/ts/models/message.ts b/ts/models/message.ts index 69386ec1b..842d64db8 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -447,8 +447,10 @@ export class MessageModel extends Backbone.Model { return 'read'; } const sent = this.get('sent'); + // control messages we've sent, synced from the network appear to just have the sent_at field set. + const sentAt = this.get('sent_at'); const sentTo = this.get('sent_to') || []; - if (sent || sentTo.length > 0) { + if (sent || sentTo.length > 0 || sentAt) { return 'sent'; } diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index 9d3bd53b2..abc67ceaa 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -1431,7 +1431,7 @@ function getBlockedNumbersDuringMigration(db: BetterSqlite3.Database) { try { const blockedItem = sqlNode.getItemById('blocked', db); if (!blockedItem) { - throw new Error('no blocked contacts at all'); + return []; } const foundBlocked = blockedItem?.value; console.info('foundBlockedNumbers during migration', foundBlocked); @@ -1497,6 +1497,15 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite WHERE type = 'private' AND (active_at IS NULL OR active_at = 0 );` ).run({}); + // create the table which is going to handle the wrappers, without any content in this migration. + db.exec(`CREATE TABLE ${CONFIG_DUMP_TABLE}( + variant TEXT NOT NULL, + publicKey TEXT NOT NULL, + data BLOB, + PRIMARY KEY (publicKey, variant) + ); + `); + /** * Remove the `publicChat` prefix from the communities, instead keep the full url+room in it, with the corresponding http or https prefix. * This is easier to handle with the libsession wrappers @@ -1531,7 +1540,7 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite newId, oldId: convoDetails.oldConvoId, }); - // do the same for messages and where else? + // do the same for messages db.prepare( `UPDATE ${MESSAGES_TABLE} SET @@ -1543,19 +1552,11 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite db.prepare( `UPDATE ${OPEN_GROUP_ROOMS_V2_TABLE} SET conversationId = $newId, - json = json_set(json, '$.conversationId', $newId);` - ).run({ newId }); + json = json_set(json, '$.conversationId', $newId) + WHERE conversationId = $oldConvoId;` + ).run({ newId, oldConvoId: convoDetails.oldConvoId }); }); - // create the table which is going to handle the wrappers, without any content in this migration. - db.exec(`CREATE TABLE ${CONFIG_DUMP_TABLE}( - variant TEXT NOT NULL, - publicKey TEXT NOT NULL, - data BLOB, - PRIMARY KEY (publicKey, variant) - ); - `); - writeSessionSchemaVersion(targetVersion, db); })(); @@ -1712,7 +1713,6 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite communitiesToWriteInWrapper.forEach(community => { try { - console.info('Writing community: ', JSON.stringify(community)); insertCommunityIntoWrapper( community, userGroupsConfigWrapper, @@ -1810,7 +1810,7 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite // still, we update the schema version writeSessionSchemaVersion(targetVersion, db); - }); + })(); } export function printTableColumns(table: string, db: BetterSqlite3.Database) { diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 8ef8b222f..981f7553a 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -342,6 +342,8 @@ export async function markGroupAsLeftOrKicked( groupConvo: ConversationModel, isKicked: boolean ) { + getSwarmPollingInstance().removePubkey(groupPublicKey); + await innerRemoveAllClosedGroupEncryptionKeyPairs(groupPublicKey); if (isKicked) { @@ -349,7 +351,6 @@ export async function markGroupAsLeftOrKicked( } else { groupConvo.set('left', true); } - getSwarmPollingInstance().removePubkey(groupPublicKey); } /** @@ -547,13 +548,9 @@ async function performIfValid( } else if (groupUpdate.type === Type.MEMBER_LEFT) { await handleClosedGroupMemberLeft(envelope, convo); } else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) { - window?.log?.warn( - 'Received ENCRYPTION_KEY_PAIR_REQUEST message but it is not enabled for now.' - ); await removeFromCache(envelope); - - // if you add a case here, remember to add it where performIfValid is called too. } + // if you add a case here, remember to add it where performIfValid is called too. return true; } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 9f4ca2c14..84be29005 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -329,7 +329,15 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { for (let index = 0; index < legacyGroupsToLeaveInDB.length; index++) { const toLeave = legacyGroupsToLeaveInDB[index]; console.warn('leaving legacy group from configuration sync message with convoId ', toLeave.id); - await getConversationController().deleteContact(toLeave.id, true); + 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, true); + } } for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) { @@ -379,10 +387,6 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { let changes = await legacyGroupConvo.setPriorityFromWrapper(fromWrapper.priority, false); - if (legacyGroupConvo.get('priority') !== fromWrapper.priority) { - legacyGroupConvo.set({ priority: fromWrapper.priority }); - changes = true; - } const existingTimestampMs = legacyGroupConvo.get('lastJoinedTimestamp'); if (Math.floor(existingTimestampMs / 1000) !== fromWrapper.joinedAtSeconds) { legacyGroupConvo.set({ lastJoinedTimestamp: fromWrapper.joinedAtSeconds * 1000 }); @@ -400,29 +404,31 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { ); changes = true; } - // start polling for this new group - getSwarmPollingInstance().addGroupId(PubKey.cast(fromWrapper.pubkeyHex)); + // start polling for this group if we haven't left it yet. The wrapper does not store this info for legacy group so we check from the DB entry instead + if (!legacyGroupConvo.get('isKickedFromGroup') && !legacyGroupConvo.get('left')) { + getSwarmPollingInstance().addGroupId(PubKey.cast(fromWrapper.pubkeyHex)); - // trigger decrypting of all this group messages we did not decrypt successfully yet. - await queueAllCachedFromSource(fromWrapper.pubkeyHex); + // save the encryption keypair if needed + if (!isEmpty(fromWrapper.encPubkey) && !isEmpty(fromWrapper.encSeckey)) { + try { + const inWrapperKeypair: HexKeyPair = { + publicHex: toHex(fromWrapper.encPubkey), + privateHex: toHex(fromWrapper.encSeckey), + }; + + await addKeyPairToCacheAndDBIfNeeded(fromWrapper.pubkeyHex, inWrapperKeypair); + } catch (e) { + window.log.warn('failed to save keypair for legacugroup', fromWrapper.pubkeyHex); + } + } + } if (changes) { await legacyGroupConvo.commit(); } - // save the encryption keypair if needed - if (!isEmpty(fromWrapper.encPubkey) && !isEmpty(fromWrapper.encSeckey)) { - try { - const inWrapperKeypair: HexKeyPair = { - publicHex: toHex(fromWrapper.encPubkey), - privateHex: toHex(fromWrapper.encSeckey), - }; - - await addKeyPairToCacheAndDBIfNeeded(fromWrapper.pubkeyHex, inWrapperKeypair); - } catch (e) { - window.log.warn('failed to save keypair for legacugroup', fromWrapper.pubkeyHex); - } - } + // trigger decrypting of all this group messages we did not decrypt successfully yet. + await queueAllCachedFromSource(fromWrapper.pubkeyHex); } } @@ -458,9 +464,9 @@ async function applyConvoVolatileUpdateFromWrapper( } try { - window.log.debug( - `applyConvoVolatileUpdateFromWrapper: ${convoId}: forcedUnread:${forcedUnread}, lastReadMessage:${lastReadMessageTimestamp}` - ); + // window.log.debug( + // `applyConvoVolatileUpdateFromWrapper: ${convoId}: forcedUnread:${forcedUnread}, lastReadMessage:${lastReadMessageTimestamp}` + // ); // this should mark all the messages sent before fromWrapper.lastRead as read and update the unreadCount await foundConvo.markReadFromConfigMessage(lastReadMessageTimestamp); // this commits to the DB, if needed @@ -639,11 +645,13 @@ async function handleConfigMessagesViaLibSession( } window?.log?.debug( - `Handling our sharedConfig message via libsession_util ${configMessages.map(m => ({ - variant: LibSessionUtil.kindToVariant(m.message.kind), - hash: m.messageHash, - seqno: (m.message.seqno as Long).toNumber(), - }))}` + `Handling our sharedConfig message via libsession_util ${JSON.stringify( + configMessages.map(m => ({ + variant: LibSessionUtil.kindToVariant(m.message.kind), + hash: m.messageHash, + seqno: (m.message.seqno as Long).toNumber(), + })) + )}` ); const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessages); diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 58c5d737a..3fe517954 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -1,20 +1,20 @@ +import { v4 as uuidv4 } from 'uuid'; import { EnvelopePlus } from './types'; export { downloadAttachment } from './attachments'; -import { v4 as uuidv4 } from 'uuid'; import { addToCache, getAllFromCache, getAllFromCacheForSource, removeFromCache } from './cache'; // innerHandleSwarmContentMessage is only needed because of code duplication in handleDecryptedEnvelope... -import { handleSwarmContentMessage, innerHandleSwarmContentMessage } from './contentMessage'; import _ from 'lodash'; +import { handleSwarmContentMessage, innerHandleSwarmContentMessage } from './contentMessage'; -import { getEnvelopeId } from './common'; -import { StringUtils, UserUtils } from '../session/utils'; -import { SignalService } from '../protobuf'; import { Data } from '../data/data'; -import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; +import { SignalService } from '../protobuf'; +import { StringUtils, UserUtils } from '../session/utils'; import { perfEnd, perfStart } from '../session/utils/Performance'; +import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; import { UnprocessedParameter } from '../types/sqlSharedTypes'; +import { getEnvelopeId } from './common'; const incomingMessagePromises: Array> = []; @@ -147,9 +147,11 @@ export function handleRequest( */ export async function queueAllCached() { const items = await getAllFromCache(); - items.forEach(item => { - void queueCached(item); - }); + + await items.reduce(async (promise, item) => { + await promise; + await queueCached(item); + }, Promise.resolve()); } export async function queueAllCachedFromSource(source: string) { diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 73c6c44df..d2035e719 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -23,7 +23,10 @@ import { SnodeAPIRetrieve } from './retrieveRequest'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; import { ReleasedFeatures } from '../../../util/releaseFeature'; import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; -import { GenericWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; +import { + GenericWrapperActions, + UserGroupsWrapperActions, +} from '../../../webworker/workers/browser/libsession_worker_interface'; export function extractWebSocketContent( message: string, @@ -171,13 +174,13 @@ export class SwarmPolling { ?.idForLogging() || group.pubkey.key; if (diff >= convoPollingTimeout) { - window?.log?.info( + window?.log?.debug( `Polling for ${loggingId}; timeout: ${convoPollingTimeout}; diff: ${diff} ` ); return this.pollOnceForKey(group.pubkey, true, [SnodeNamespaces.ClosedGroupMessage]); } - window?.log?.info( + window?.log?.debug( `Not polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}` ); @@ -186,7 +189,7 @@ export class SwarmPolling { try { await Promise.all(concat([directPromise], groupPromises)); } catch (e) { - window?.log?.info('pollForAllKeys exception: ', e); + window?.log?.warn('pollForAllKeys exception: ', e); throw e; } finally { setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE); @@ -270,7 +273,7 @@ export class SwarmPolling { ); } if (allNamespacesWithoutUserConfigIfNeeded.length) { - window.log.info( + window.log.debug( `received allNamespacesWithoutUserConfigIfNeeded: ${allNamespacesWithoutUserConfigIfNeeded.length}` ); } @@ -280,7 +283,7 @@ export class SwarmPolling { // if all snodes returned an error (null), no need to update the lastPolledTimestamp if (isGroup) { - window?.log?.info( + window?.log?.debug( `Polled for group(${ed25519Str(pubkey.key)}):, got ${messages.length} messages back.` ); let lastPolledTimestamp = Date.now(); @@ -306,15 +309,28 @@ export class SwarmPolling { const newMessages = await this.handleSeenMessages(messages); perfEnd(`handleSeenMessages-${polledPubkey}`, 'handleSeenMessages'); - // trigger the handling of all the other messages, not shared config related - newMessages.forEach(m => { - const content = extractWebSocketContent(m.data, m.hash); - if (!content) { - return; - } + // don't handle incoming messages from group swarms when using the userconfig and the group is not one of the tracked group + const isUserConfigReleaseLive = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + if ( + isUserConfigReleaseLive && + isGroup && + polledPubkey.startsWith('05') && + !(await UserGroupsWrapperActions.getLegacyGroup(polledPubkey)) // just check if a legacy group with that name exists + ) { + // that pubkey is not tracked in the wrapper anymore. Just discard those messages and make sure we are not polling + // TODOLATER we might need to do something like this for the new closed groups once released + await getSwarmPollingInstance().removePubkey(polledPubkey); + } else { + // trigger the handling of all the other messages, not shared config related + newMessages.forEach(m => { + const content = extractWebSocketContent(m.data, m.hash); + if (!content) { + return; + } - Receiver.handleRequest(content.body, isGroup ? polledPubkey : null, content.messageHash); - }); + Receiver.handleRequest(content.body, isGroup ? polledPubkey : null, content.messageHash); + }); + } } private async handleSharedConfigMessages(userConfigMessagesMerged: Array) { diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index cc56ba2b8..1d37bb9b2 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -12,12 +12,18 @@ import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/con import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes'; import { assertUnreachable } from '../../types/sqlSharedTypes'; import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; -import { leaveClosedGroup } from '../group/closed-group'; import { ConfigurationSync } from '../utils/job_runners/jobs/ConfigurationSyncJob'; import { LibSessionUtil } from '../utils/libsession/libsession_utils'; import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts'; import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_utils_convo_info_volatile'; import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups'; +import { 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'; let instance: ConversationController | null; @@ -262,6 +268,7 @@ export class ConversationController { if (roomInfos) { getOpenGroupManager().removeRoomFromPolledRooms(roomInfos); } + await this.cleanUpGroupConversation(conversation.id); // remove the roomInfos locally for this open group room including the pubkey try { @@ -272,34 +279,13 @@ export class ConversationController { break; case 'LegacyGroup': window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`); - await leaveClosedGroup(conversation.id); - await SessionUtilUserGroups.removeLegacyGroupFromWrapper(conversation.id); - await SessionUtilConvoInfoVolatile.removeLegacyGroupFromWrapper(conversation.id); - + await leaveClosedGroup(conversation.id, 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 (conversation.isGroup()) { - window.log.info(`deleteContact isGroup, removing convo from DB: ${id}`); - // not a private conversation, so not a contact for the ContactWrapper - await Data.removeConversation(id); - - window.log.info(`deleteContact isGroup, convo removed from DB: ${id}`); - this.conversations.remove(conversation); - - window?.inboxStore?.dispatch( - conversationActions.conversationChanged({ - id: conversation.id, - data: conversation.getConversationModelProps(), - }) - ); - window.inboxStore?.dispatch(conversationActions.conversationRemoved(conversation.id)); - - window.log.info(`deleteContact NOT private, convo removed from store: ${id}`); - } - if (!fromSyncMessage) { await ConfigurationSync.queueNewJobIfNeeded(); } @@ -400,4 +386,110 @@ export class ConversationController { } this.conversations.reset([]); } + + private async cleanUpGroupConversation(id: string) { + window.log.info(`deleteContact isGroup, removing convo from DB: ${id}`); + // not a private conversation, so not a contact for the ContactWrapper + await Data.removeConversation(id); + + window.log.info(`deleteContact isGroup, convo removed from DB: ${id}`); + const conversation = this.conversations.get(id); + + if (conversation) { + this.conversations.remove(conversation); + + window?.inboxStore?.dispatch( + conversationActions.conversationChanged({ + id: id, + data: conversation.getConversationModelProps(), + }) + ); + } + window.inboxStore?.dispatch(conversationActions.conversationRemoved(id)); + + window.log.info(`deleteContact NOT private, convo removed from store: ${id}`); + } +} + +/** + * 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. + * + * 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. + */ +async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { + const convo = getConversationController().get(groupId); + + if (!convo || !convo.isClosedGroup()) { + window?.log?.error('Cannot leave non-existing group'); + return; + } + + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourNumber); + + let members: Array = []; + let admins: Array = []; + + // if we are the admin, the group must be destroyed for every members + if (isCurrentUserAdmin) { + window?.log?.info('Admin left a closed group. We need to destroy it'); + convo.set({ left: true }); + members = []; + admins = []; + } else { + // otherwise, just the exclude ourself from the members and trigger an update with this + convo.set({ left: true }); + members = (convo.get('members') || []).filter((m: string) => m !== ourNumber); + admins = convo.get('groupAdmins') || []; + } + convo.set({ members }); + 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) { + // Send the update to the group + 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.`); + // sent the message to the group and once done, remove everything related to this group + const wasSent = await getMessageQueue().sendToPubKeyNonDurably({ + message: ourLeavingMessage, + namespace: SnodeNamespaces.ClosedGroupMessage, + pubkey: PubKey.cast(groupId), + }); + window?.log?.info( + `Leaving message sent ${groupId}. Removing everything related to this group.` + ); + if (wasSent) { + await cleanUpFullyLeftLegacyGroup(groupId); + } + } else { + await cleanUpFullyLeftLegacyGroup(groupId); + } +} + +async function cleanUpFullyLeftLegacyGroup(groupId: string) { + const convo = getConversationController().get(groupId); + + await UserGroupsWrapperActions.eraseLegacyGroup(groupId); + await SessionUtilConvoInfoVolatile.removeLegacyGroupFromWrapper(groupId); + if (convo) { + await markGroupAsLeftOrKicked(groupId, convo, false); + } } diff --git a/ts/session/conversations/createClosedGroup.ts b/ts/session/conversations/createClosedGroup.ts index f95184578..e2fe7f308 100644 --- a/ts/session/conversations/createClosedGroup.ts +++ b/ts/session/conversations/createClosedGroup.ts @@ -8,11 +8,7 @@ import { openConversationWithMessages } from '../../state/ducks/conversations'; import { updateConfirmModal } from '../../state/ducks/modalDialog'; import { getSwarmPollingInstance } from '../apis/snode_api'; import { SnodeNamespaces } from '../apis/snode_api/namespaces'; -import { - generateClosedGroupPublicKey, - generateCurve25519KeyPairWithoutPrefix, - generateGroupV3Keypair, -} from '../crypto'; +import { generateClosedGroupPublicKey, generateCurve25519KeyPairWithoutPrefix } from '../crypto'; import { ClosedGroupNewMessage, ClosedGroupNewMessageParams, @@ -31,13 +27,14 @@ export async function createClosedGroup(groupName: string, members: Array = []; - let admins: Array = []; - - // if we are the admin, the group must be destroyed for every members - if (isCurrentUserAdmin) { - window?.log?.info('Admin left a closed group. We need to destroy it'); - convo.set({ left: true }); - members = []; - admins = []; - } else { - // otherwise, just the exclude ourself from the members and trigger an update with this - convo.set({ left: true }); - members = (convo.get('members') || []).filter((m: string) => m !== ourNumber.key); - admins = convo.get('groupAdmins') || []; - } - convo.set({ members }); - 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, - }); - // Send the update to the group - 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.`); - // sent the message to the group and once done, remove everything related to this group - getSwarmPollingInstance().removePubkey(groupId); - await getMessageQueue().sendToGroup({ - message: ourLeavingMessage, - namespace: SnodeNamespaces.ClosedGroupMessage, - sentCb: async () => { - window?.log?.info( - `Leaving message sent ${groupId}. Removing everything related to this group.` - ); - await markGroupAsLeftOrKicked(groupId, convo, false); - }, - }); -} - async function sendNewName(convo: ConversationModel, name: string, messageId: string) { if (name.length === 0) { window?.log?.warn('No name given for group update. Skipping'); diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 68c93254b..99eaf4f36 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -231,7 +231,11 @@ export class MessageQueue { pubkey, }: { pubkey: PubKey; - message: ClosedGroupNewMessage | CallMessage | SharedConfigMessage; + message: + | ClosedGroupNewMessage + | CallMessage + | SharedConfigMessage + | ClosedGroupMemberLeftMessage; namespace: SnodeNamespaces; }): Promise { let rawMessage; diff --git a/ts/session/utils/libsession/libsession_utils_user_groups.ts b/ts/session/utils/libsession/libsession_utils_user_groups.ts index 65aea766b..ea7aa3d57 100644 --- a/ts/session/utils/libsession/libsession_utils_user_groups.ts +++ b/ts/session/utils/libsession/libsession_utils_user_groups.ts @@ -10,6 +10,7 @@ import { import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; import { OpenGroupUtils } from '../../apis/open_group_api/utils'; import { getConversationController } from '../../conversations'; +import { isEmpty } from 'lodash'; /** * The key of this map is the convoId as stored in the database. @@ -129,9 +130,18 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise try { window.log.debug(`inserting into usergroup wrapper "${foundConvo.id}"... }`); + // this does the create or the update of the matching existing legacy group - await UserGroupsWrapperActions.setLegacyGroup(wrapperLegacyGroup); + if ( + !isEmpty(wrapperLegacyGroup.name) && + !isEmpty(wrapperLegacyGroup.encPubkey) && + !isEmpty(wrapperLegacyGroup.encSeckey) + ) { + console.warn('inserting into user wrapper', wrapperLegacyGroup); + await UserGroupsWrapperActions.setLegacyGroup(wrapperLegacyGroup); + } + await refreshCachedUserGroup(convoId); } catch (e) { window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index b94781b67..8d25b4173 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -377,7 +377,7 @@ export const fetchTopMessagesForConversation = createAsyncThunk( const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey); if (!oldestMessage || oldestMessage.id === oldTopMessageId) { - window.log.debug('fetchTopMessagesForConversation: we are already at the top'); + // window.log.debug('fetchTopMessagesForConversation: we are already at the top'); return null; } const messagesProps = await getMessages({ @@ -414,7 +414,7 @@ export const fetchBottomMessagesForConversation = createAsyncThunk( const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey); if (!mostRecentMessage || mostRecentMessage.id === oldBottomMessageId) { - window.log.debug('fetchBottomMessagesForConversation: we are already at the bottom'); + // window.log.debug('fetchBottomMessagesForConversation: we are already at the bottom'); return null; } const messagesProps = await getMessages({ diff --git a/ts/util/privacy.ts b/ts/util/privacy.ts index 9085d5fe8..2ab8a5e36 100644 --- a/ts/util/privacy.ts +++ b/ts/util/privacy.ts @@ -2,7 +2,7 @@ // tslint:disable-next-line: no-submodule-imports import { compose } from 'lodash/fp'; -import { escapeRegExp, isRegExp, isString } from 'lodash'; +import { escapeRegExp, isNil, isRegExp, isString } from 'lodash'; import { getAppRootPath } from '../node/getRootPath'; const APP_ROOT_PATH = getAppRootPath(); @@ -98,9 +98,16 @@ const removeNewlines = (text: string) => text.replace(/\r?\n|\r/g, ''); // redactSensitivePaths :: String -> String const redactSensitivePaths = redactPath(APP_ROOT_PATH); -const isDev = (process.env.NODE_APP_INSTANCE || '').startsWith('devprod'); +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; + } + // otherwise we don't want to redact logs when running on the devprod env + return (process.env.NODE_APP_INSTANCE || '').startsWith('devprod'); +} // redactAll :: String -> String -export const redactAll = !isDev +export const redactAll = !shouldNotRedactLogs() ? compose(redactSensitivePaths, redactGroupIds, redactSessionID, redactSnodeIP, redactServerUrl) : (text: string) => text;