fix: skip new group messages when receved after group was removed

pull/2620/head
Audric Ackermann 2 years ago
parent bac2887c28
commit 6f6620f622

@ -1,51 +1,54 @@
import React from 'react'; import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { leaveClosedGroup } from '../../session/group/closed-group';
import { adminLeaveClosedGroup } from '../../state/ducks/modalDialog'; import { adminLeaveClosedGroup } from '../../state/ducks/modalDialog';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SpacerLG } from '../basic/Text'; import { SpacerLG } from '../basic/Text';
import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionSpinner } from '../basic/SessionSpinner';
type Props = {
conversationId: string;
};
const StyledWarning = styled.p` const StyledWarning = styled.p`
max-width: 500px; max-width: 500px;
line-height: 1.3333; 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 convo = getConversationController().get(props.conversationId);
const titleText = `${window.i18n('leaveGroup')} ${convo.getRealSessionUsername()}`; const [loading, setLoading] = useState(false);
const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`; const titleText = `${window.i18n('leaveGroup')} ${convo?.getRealSessionUsername() || ''}`;
const okText = window.i18n('leaveAndRemoveForEveryone');
const cancelText = window.i18n('cancel');
const onClickOK = async () => { const closeDialog = () => {
await leaveClosedGroup(props.conversationId); dispatch(adminLeaveClosedGroup(null));
closeDialog();
}; };
const closeDialog = () => { const onClickOK = async () => {
window.inboxStore?.dispatch(adminLeaveClosedGroup(null)); 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 ( return (
<SessionWrapperModal title={titleText} onClose={closeDialog}> <SessionWrapperModal title={titleText} onClose={closeDialog}>
<SpacerLG /> <SpacerLG />
<StyledWarning>{warningAsAdmin}</StyledWarning> <StyledWarning>{window.i18n('leaveGroupConfirmationAdmin')}</StyledWarning>
<SessionSpinner loading={loading} />
<div className="session-modal__button-group"> <div className="session-modal__button-group">
<SessionButton <SessionButton
text={okText} text={window.i18n('leaveAndRemoveForEveryone')}
buttonColor={SessionButtonColor.Danger} buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple} buttonType={SessionButtonType.Simple}
onClick={onClickOK} onClick={onClickOK}
/> />
<SessionButton <SessionButton
text={cancelText} text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple} buttonType={SessionButtonType.Simple}
onClick={closeDialog} onClick={closeDialog}
/> />

@ -7,13 +7,16 @@ import { CallManager, SyncUtils, ToastUtils, UserUtils } from '../session/utils'
import { SessionButtonColor } from '../components/basic/SessionButton'; import { SessionButtonColor } from '../components/basic/SessionButton';
import { getCallMediaPermissionsSettings } from '../components/settings/SessionSettings'; import { getCallMediaPermissionsSettings } from '../components/settings/SessionSettings';
import { Data } from '../data/data'; import { Data } from '../data/data';
import { SettingsKey } from '../data/settings-key';
import { uploadFileToFsWithOnionV4 } from '../session/apis/file_server_api/FileServerApi'; import { uploadFileToFsWithOnionV4 } from '../session/apis/file_server_api/FileServerApi';
import { OpenGroupUtils } from '../session/apis/open_group_api/utils';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
import { getSodiumRenderer } from '../session/crypto'; import { getSodiumRenderer } from '../session/crypto';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob';
import { perfEnd, perfStart } from '../session/utils/Performance'; import { perfEnd, perfStart } from '../session/utils/Performance';
import { fromHexToArray, toHex } from '../session/utils/String'; 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 { forceSyncConfigurationNowIfNeeded } from '../session/utils/sync/syncUtils';
import { import {
conversationReset, conversationReset,
@ -32,17 +35,13 @@ import {
updateRemoveModeratorsModal, updateRemoveModeratorsModal,
} from '../state/ducks/modalDialog'; } from '../state/ducks/modalDialog';
import { MIME } from '../types'; import { MIME } from '../types';
import { urlToBlob } from '../types/attachments/VisualAttachment';
import { processNewAttachment } from '../types/MessageAttachment';
import { IMAGE_JPEG } from '../types/MIME'; import { IMAGE_JPEG } from '../types/MIME';
import { processNewAttachment } from '../types/MessageAttachment';
import { urlToBlob } from '../types/attachments/VisualAttachment';
import { BlockedNumberController } from '../util/blockedNumberController'; import { BlockedNumberController } from '../util/blockedNumberController';
import { encryptProfile } from '../util/crypto/profileEncrypter'; 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 { ReleasedFeatures } from '../util/releaseFeature';
import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface'; import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
export async function copyPublicKeyByConvoId(convoId: string) { export async function copyPublicKeyByConvoId(convoId: string) {
@ -250,19 +249,19 @@ export function showLeaveGroupByConvoId(conversationId: string) {
title, title,
message, message,
onClickOk: async () => { onClickOk: async () => {
await leaveClosedGroup(conversation.id); await getConversationController().deleteContact(conversation.id, false);
onClickClose(); onClickClose();
}, },
onClickClose, onClickClose,
}) })
); );
} else { return;
}
window.inboxStore?.dispatch( window.inboxStore?.dispatch(
adminLeaveClosedGroup({ adminLeaveClosedGroup({
conversationId, conversationId,
}) })
); );
}
} }
export function showInviteContactByConvoId(conversationId: string) { export function showInviteContactByConvoId(conversationId: string) {
window.inboxStore?.dispatch(updateInviteContactModal({ conversationId })); window.inboxStore?.dispatch(updateInviteContactModal({ conversationId }));

@ -447,8 +447,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return 'read'; return 'read';
} }
const sent = this.get('sent'); 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') || []; const sentTo = this.get('sent_to') || [];
if (sent || sentTo.length > 0) { if (sent || sentTo.length > 0 || sentAt) {
return 'sent'; return 'sent';
} }

@ -1431,7 +1431,7 @@ function getBlockedNumbersDuringMigration(db: BetterSqlite3.Database) {
try { try {
const blockedItem = sqlNode.getItemById('blocked', db); const blockedItem = sqlNode.getItemById('blocked', db);
if (!blockedItem) { if (!blockedItem) {
throw new Error('no blocked contacts at all'); return [];
} }
const foundBlocked = blockedItem?.value; const foundBlocked = blockedItem?.value;
console.info('foundBlockedNumbers during migration', foundBlocked); 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 );` WHERE type = 'private' AND (active_at IS NULL OR active_at = 0 );`
).run({}); ).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. * 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 * This is easier to handle with the libsession wrappers
@ -1531,7 +1540,7 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite
newId, newId,
oldId: convoDetails.oldConvoId, oldId: convoDetails.oldConvoId,
}); });
// do the same for messages and where else? // do the same for messages
db.prepare( db.prepare(
`UPDATE ${MESSAGES_TABLE} SET `UPDATE ${MESSAGES_TABLE} SET
@ -1543,19 +1552,11 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite
db.prepare( db.prepare(
`UPDATE ${OPEN_GROUP_ROOMS_V2_TABLE} SET `UPDATE ${OPEN_GROUP_ROOMS_V2_TABLE} SET
conversationId = $newId, conversationId = $newId,
json = json_set(json, '$.conversationId', $newId);` json = json_set(json, '$.conversationId', $newId)
).run({ 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); writeSessionSchemaVersion(targetVersion, db);
})(); })();
@ -1712,7 +1713,6 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
communitiesToWriteInWrapper.forEach(community => { communitiesToWriteInWrapper.forEach(community => {
try { try {
console.info('Writing community: ', JSON.stringify(community));
insertCommunityIntoWrapper( insertCommunityIntoWrapper(
community, community,
userGroupsConfigWrapper, userGroupsConfigWrapper,
@ -1810,7 +1810,7 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
// still, we update the schema version // still, we update the schema version
writeSessionSchemaVersion(targetVersion, db); writeSessionSchemaVersion(targetVersion, db);
}); })();
} }
export function printTableColumns(table: string, db: BetterSqlite3.Database) { export function printTableColumns(table: string, db: BetterSqlite3.Database) {

@ -342,6 +342,8 @@ export async function markGroupAsLeftOrKicked(
groupConvo: ConversationModel, groupConvo: ConversationModel,
isKicked: boolean isKicked: boolean
) { ) {
getSwarmPollingInstance().removePubkey(groupPublicKey);
await innerRemoveAllClosedGroupEncryptionKeyPairs(groupPublicKey); await innerRemoveAllClosedGroupEncryptionKeyPairs(groupPublicKey);
if (isKicked) { if (isKicked) {
@ -349,7 +351,6 @@ export async function markGroupAsLeftOrKicked(
} else { } else {
groupConvo.set('left', true); groupConvo.set('left', true);
} }
getSwarmPollingInstance().removePubkey(groupPublicKey);
} }
/** /**
@ -547,13 +548,9 @@ async function performIfValid(
} else if (groupUpdate.type === Type.MEMBER_LEFT) { } else if (groupUpdate.type === Type.MEMBER_LEFT) {
await handleClosedGroupMemberLeft(envelope, convo); await handleClosedGroupMemberLeft(envelope, convo);
} else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) { } 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); 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; return true;
} }

@ -329,8 +329,16 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
for (let index = 0; index < legacyGroupsToLeaveInDB.length; index++) { for (let index = 0; index < legacyGroupsToLeaveInDB.length; index++) {
const toLeave = legacyGroupsToLeaveInDB[index]; const toLeave = legacyGroupsToLeaveInDB[index];
console.warn('leaving legacy group from configuration sync message with convoId ', toLeave.id); console.warn('leaving legacy group from configuration sync message with convoId ', 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, true); await getConversationController().deleteContact(toLeave.id, true);
} }
}
for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) { for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) {
const toJoin = legacyGroupsToJoinInDB[index]; const toJoin = legacyGroupsToJoinInDB[index];
@ -379,10 +387,6 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
let changes = await legacyGroupConvo.setPriorityFromWrapper(fromWrapper.priority, false); 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'); const existingTimestampMs = legacyGroupConvo.get('lastJoinedTimestamp');
if (Math.floor(existingTimestampMs / 1000) !== fromWrapper.joinedAtSeconds) { if (Math.floor(existingTimestampMs / 1000) !== fromWrapper.joinedAtSeconds) {
legacyGroupConvo.set({ lastJoinedTimestamp: fromWrapper.joinedAtSeconds * 1000 }); legacyGroupConvo.set({ lastJoinedTimestamp: fromWrapper.joinedAtSeconds * 1000 });
@ -400,16 +404,10 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
); );
changes = true; changes = true;
} }
// start polling for this new group // 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)); getSwarmPollingInstance().addGroupId(PubKey.cast(fromWrapper.pubkeyHex));
// trigger decrypting of all this group messages we did not decrypt successfully yet.
await queueAllCachedFromSource(fromWrapper.pubkeyHex);
if (changes) {
await legacyGroupConvo.commit();
}
// save the encryption keypair if needed // save the encryption keypair if needed
if (!isEmpty(fromWrapper.encPubkey) && !isEmpty(fromWrapper.encSeckey)) { if (!isEmpty(fromWrapper.encPubkey) && !isEmpty(fromWrapper.encSeckey)) {
try { try {
@ -424,6 +422,14 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
} }
} }
} }
if (changes) {
await legacyGroupConvo.commit();
}
// trigger decrypting of all this group messages we did not decrypt successfully yet.
await queueAllCachedFromSource(fromWrapper.pubkeyHex);
}
} }
async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> { async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
@ -458,9 +464,9 @@ async function applyConvoVolatileUpdateFromWrapper(
} }
try { try {
window.log.debug( // window.log.debug(
`applyConvoVolatileUpdateFromWrapper: ${convoId}: forcedUnread:${forcedUnread}, lastReadMessage:${lastReadMessageTimestamp}` // `applyConvoVolatileUpdateFromWrapper: ${convoId}: forcedUnread:${forcedUnread}, lastReadMessage:${lastReadMessageTimestamp}`
); // );
// this should mark all the messages sent before fromWrapper.lastRead as read and update the unreadCount // this should mark all the messages sent before fromWrapper.lastRead as read and update the unreadCount
await foundConvo.markReadFromConfigMessage(lastReadMessageTimestamp); await foundConvo.markReadFromConfigMessage(lastReadMessageTimestamp);
// this commits to the DB, if needed // this commits to the DB, if needed
@ -639,11 +645,13 @@ async function handleConfigMessagesViaLibSession(
} }
window?.log?.debug( window?.log?.debug(
`Handling our sharedConfig message via libsession_util ${configMessages.map(m => ({ `Handling our sharedConfig message via libsession_util ${JSON.stringify(
configMessages.map(m => ({
variant: LibSessionUtil.kindToVariant(m.message.kind), variant: LibSessionUtil.kindToVariant(m.message.kind),
hash: m.messageHash, hash: m.messageHash,
seqno: (m.message.seqno as Long).toNumber(), seqno: (m.message.seqno as Long).toNumber(),
}))}` }))
)}`
); );
const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessages); const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessages);

@ -1,20 +1,20 @@
import { v4 as uuidv4 } from 'uuid';
import { EnvelopePlus } from './types'; import { EnvelopePlus } from './types';
export { downloadAttachment } from './attachments'; export { downloadAttachment } from './attachments';
import { v4 as uuidv4 } from 'uuid';
import { addToCache, getAllFromCache, getAllFromCacheForSource, removeFromCache } from './cache'; import { addToCache, getAllFromCache, getAllFromCacheForSource, removeFromCache } from './cache';
// innerHandleSwarmContentMessage is only needed because of code duplication in handleDecryptedEnvelope... // innerHandleSwarmContentMessage is only needed because of code duplication in handleDecryptedEnvelope...
import { handleSwarmContentMessage, innerHandleSwarmContentMessage } from './contentMessage';
import _ from 'lodash'; 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 { 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 { perfEnd, perfStart } from '../session/utils/Performance';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
import { UnprocessedParameter } from '../types/sqlSharedTypes'; import { UnprocessedParameter } from '../types/sqlSharedTypes';
import { getEnvelopeId } from './common';
const incomingMessagePromises: Array<Promise<any>> = []; const incomingMessagePromises: Array<Promise<any>> = [];
@ -147,9 +147,11 @@ export function handleRequest(
*/ */
export async function queueAllCached() { export async function queueAllCached() {
const items = await getAllFromCache(); 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) { export async function queueAllCachedFromSource(source: string) {

@ -23,7 +23,10 @@ import { SnodeAPIRetrieve } from './retrieveRequest';
import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types';
import { ReleasedFeatures } from '../../../util/releaseFeature'; import { ReleasedFeatures } from '../../../util/releaseFeature';
import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; 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( export function extractWebSocketContent(
message: string, message: string,
@ -171,13 +174,13 @@ export class SwarmPolling {
?.idForLogging() || group.pubkey.key; ?.idForLogging() || group.pubkey.key;
if (diff >= convoPollingTimeout) { if (diff >= convoPollingTimeout) {
window?.log?.info( window?.log?.debug(
`Polling for ${loggingId}; timeout: ${convoPollingTimeout}; diff: ${diff} ` `Polling for ${loggingId}; timeout: ${convoPollingTimeout}; diff: ${diff} `
); );
return this.pollOnceForKey(group.pubkey, true, [SnodeNamespaces.ClosedGroupMessage]); return this.pollOnceForKey(group.pubkey, true, [SnodeNamespaces.ClosedGroupMessage]);
} }
window?.log?.info( window?.log?.debug(
`Not polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}` `Not polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}`
); );
@ -186,7 +189,7 @@ export class SwarmPolling {
try { try {
await Promise.all(concat([directPromise], groupPromises)); await Promise.all(concat([directPromise], groupPromises));
} catch (e) { } catch (e) {
window?.log?.info('pollForAllKeys exception: ', e); window?.log?.warn('pollForAllKeys exception: ', e);
throw e; throw e;
} finally { } finally {
setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE); setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE);
@ -270,7 +273,7 @@ export class SwarmPolling {
); );
} }
if (allNamespacesWithoutUserConfigIfNeeded.length) { if (allNamespacesWithoutUserConfigIfNeeded.length) {
window.log.info( window.log.debug(
`received allNamespacesWithoutUserConfigIfNeeded: ${allNamespacesWithoutUserConfigIfNeeded.length}` `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 all snodes returned an error (null), no need to update the lastPolledTimestamp
if (isGroup) { if (isGroup) {
window?.log?.info( window?.log?.debug(
`Polled for group(${ed25519Str(pubkey.key)}):, got ${messages.length} messages back.` `Polled for group(${ed25519Str(pubkey.key)}):, got ${messages.length} messages back.`
); );
let lastPolledTimestamp = Date.now(); let lastPolledTimestamp = Date.now();
@ -306,6 +309,18 @@ export class SwarmPolling {
const newMessages = await this.handleSeenMessages(messages); const newMessages = await this.handleSeenMessages(messages);
perfEnd(`handleSeenMessages-${polledPubkey}`, 'handleSeenMessages'); perfEnd(`handleSeenMessages-${polledPubkey}`, 'handleSeenMessages');
// 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 // trigger the handling of all the other messages, not shared config related
newMessages.forEach(m => { newMessages.forEach(m => {
const content = extractWebSocketContent(m.data, m.hash); const content = extractWebSocketContent(m.data, m.hash);
@ -316,6 +331,7 @@ export class SwarmPolling {
Receiver.handleRequest(content.body, isGroup ? polledPubkey : null, content.messageHash); Receiver.handleRequest(content.body, isGroup ? polledPubkey : null, content.messageHash);
}); });
} }
}
private async handleSharedConfigMessages(userConfigMessagesMerged: Array<RetrieveMessageItem>) { private async handleSharedConfigMessages(userConfigMessagesMerged: Array<RetrieveMessageItem>) {
const extractedUserConfigMessage = compact( const extractedUserConfigMessage = compact(

@ -12,12 +12,18 @@ import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/con
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes'; import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes';
import { assertUnreachable } from '../../types/sqlSharedTypes'; import { assertUnreachable } from '../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { leaveClosedGroup } from '../group/closed-group';
import { ConfigurationSync } from '../utils/job_runners/jobs/ConfigurationSyncJob'; import { ConfigurationSync } from '../utils/job_runners/jobs/ConfigurationSyncJob';
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 { 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; let instance: ConversationController | null;
@ -262,6 +268,7 @@ export class ConversationController {
if (roomInfos) { if (roomInfos) {
getOpenGroupManager().removeRoomFromPolledRooms(roomInfos); getOpenGroupManager().removeRoomFromPolledRooms(roomInfos);
} }
await this.cleanUpGroupConversation(conversation.id);
// remove the roomInfos locally for this open group room including the pubkey // remove the roomInfos locally for this open group room including the pubkey
try { try {
@ -272,34 +279,13 @@ export class ConversationController {
break; break;
case 'LegacyGroup': case 'LegacyGroup':
window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`); window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`);
await leaveClosedGroup(conversation.id); await leaveClosedGroup(conversation.id, fromSyncMessage); // this removes the data from the group and convo volatile info
await SessionUtilUserGroups.removeLegacyGroupFromWrapper(conversation.id); await this.cleanUpGroupConversation(conversation.id);
await SessionUtilConvoInfoVolatile.removeLegacyGroupFromWrapper(conversation.id);
break; break;
default: default:
assertUnreachable(convoType, `deleteContact: convoType ${convoType} not handled`); 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) { if (!fromSyncMessage) {
await ConfigurationSync.queueNewJobIfNeeded(); await ConfigurationSync.queueNewJobIfNeeded();
} }
@ -400,4 +386,110 @@ export class ConversationController {
} }
this.conversations.reset([]); 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<string> = [];
let admins: Array<string> = [];
// 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);
}
} }

@ -8,11 +8,7 @@ import { openConversationWithMessages } from '../../state/ducks/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog'; import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { getSwarmPollingInstance } from '../apis/snode_api'; import { getSwarmPollingInstance } from '../apis/snode_api';
import { SnodeNamespaces } from '../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { import { generateClosedGroupPublicKey, generateCurve25519KeyPairWithoutPrefix } from '../crypto';
generateClosedGroupPublicKey,
generateCurve25519KeyPairWithoutPrefix,
generateGroupV3Keypair,
} from '../crypto';
import { import {
ClosedGroupNewMessage, ClosedGroupNewMessage,
ClosedGroupNewMessageParams, ClosedGroupNewMessageParams,
@ -31,13 +27,14 @@ export async function createClosedGroup(groupName: string, members: Array<string
const us = UserUtils.getOurPubKeyStrFromCache(); const us = UserUtils.getOurPubKeyStrFromCache();
const identityKeyPair = await generateGroupV3Keypair(); // const identityKeyPair = await generateGroupV3Keypair();
if (!identityKeyPair) { // if (!identityKeyPair) {
throw new Error('Could not create identity keypair for new closed group v3'); // throw new Error('Could not create identity keypair for new closed group v3');
} // }
// a v3 pubkey starts with 03 and an old one starts with 05 // a v3 pubkey starts with 03 and an old one starts with 05
const groupPublicKey = isV3 ? identityKeyPair.pubkey : await generateClosedGroupPublicKey(); const groupPublicKey = await generateClosedGroupPublicKey();
// const groupPublicKey = isV3 ? identityKeyPair.pubkey : await generateClosedGroupPublicKey();
// the first encryption keypair is generated the same for all versions of closed group // the first encryption keypair is generated the same for all versions of closed group
const encryptionKeyPair = await generateCurve25519KeyPairWithoutPrefix(); const encryptionKeyPair = await generateCurve25519KeyPairWithoutPrefix();

@ -43,6 +43,7 @@ export async function encrypt(
window?.log?.warn("Couldn't get key pair for closed group during encryption"); window?.log?.warn("Couldn't get key pair for closed group during encryption");
throw new Error("Couldn't get key pair for closed group"); throw new Error("Couldn't get key pair for closed group");
} }
const hexPubFromECKeyPair = PubKey.cast(hexEncryptionKeyPair.publicHex); const hexPubFromECKeyPair = PubKey.cast(hexEncryptionKeyPair.publicHex);
const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol( const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol(

@ -2,34 +2,30 @@ import { PubKey } from '../types';
import _ from 'lodash'; import _ from 'lodash';
import { fromHexToArray, toHex } from '../utils/String';
import { BlockedNumberController } from '../../util/blockedNumberController';
import { getConversationController } from '../conversations';
import { Data } from '../../data/data';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { SignalService } from '../../protobuf'; import { getMessageQueue } from '..';
import { generateCurve25519KeyPairWithoutPrefix } from '../crypto'; import { Data } from '../../data/data';
import { encryptUsingSessionProtocol } from '../crypto/MessageEncrypter';
import { ECKeyPair } from '../../receiver/keypairs';
import { UserUtils } from '../utils';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { ConversationModel } from '../../models/conversation'; import { ConversationModel } from '../../models/conversation';
import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes';
import { MessageModel } from '../../models/message'; import { MessageModel } from '../../models/message';
import { SignalService } from '../../protobuf';
import { import {
addKeyPairToCacheAndDBIfNeeded, addKeyPairToCacheAndDBIfNeeded,
distributingClosedGroupEncryptionKeyPairs, distributingClosedGroupEncryptionKeyPairs,
markGroupAsLeftOrKicked,
} from '../../receiver/closedGroups'; } from '../../receiver/closedGroups';
import { getMessageQueue } from '..'; import { ECKeyPair } from '../../receiver/keypairs';
import { BlockedNumberController } from '../../util/blockedNumberController';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { getConversationController } from '../conversations';
import { generateCurve25519KeyPairWithoutPrefix } from '../crypto';
import { encryptUsingSessionProtocol } from '../crypto/MessageEncrypter';
import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage'; import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage';
import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage'; import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage';
import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage'; import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage';
import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage'; import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage';
import { getSwarmPollingInstance } from '../apis/snode_api'; import { UserUtils } from '../utils';
import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes'; import { fromHexToArray, toHex } from '../utils/String';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
export type GroupInfo = { export type GroupInfo = {
id: string; id: string;
@ -258,66 +254,6 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
); );
} }
export async function leaveClosedGroup(groupId: string) {
const convo = getConversationController().get(groupId);
if (!convo || !convo.isClosedGroup()) {
window?.log?.error('Cannot leave non-existing group');
return;
}
const ourNumber = UserUtils.getOurPubKeyFromCache();
const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourNumber.key);
let members: Array<string> = [];
let admins: Array<string> = [];
// 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) { async function sendNewName(convo: ConversationModel, name: string, messageId: string) {
if (name.length === 0) { if (name.length === 0) {
window?.log?.warn('No name given for group update. Skipping'); window?.log?.warn('No name given for group update. Skipping');

@ -231,7 +231,11 @@ export class MessageQueue {
pubkey, pubkey,
}: { }: {
pubkey: PubKey; pubkey: PubKey;
message: ClosedGroupNewMessage | CallMessage | SharedConfigMessage; message:
| ClosedGroupNewMessage
| CallMessage
| SharedConfigMessage
| ClosedGroupMemberLeftMessage;
namespace: SnodeNamespaces; namespace: SnodeNamespaces;
}): Promise<boolean | number> { }): Promise<boolean | number> {
let rawMessage; let rawMessage;

@ -10,6 +10,7 @@ import {
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; import { 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 { getConversationController } from '../../conversations'; import { getConversationController } from '../../conversations';
import { isEmpty } from 'lodash';
/** /**
* The key of this map is the convoId as stored in the database. * The key of this map is the convoId as stored in the database.
@ -129,9 +130,18 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise
try { try {
window.log.debug(`inserting into usergroup wrapper "${foundConvo.id}"... }`); window.log.debug(`inserting into usergroup wrapper "${foundConvo.id}"... }`);
// this does the create or the update of the matching existing legacy group // this does the create or the update of the matching existing legacy group
if (
!isEmpty(wrapperLegacyGroup.name) &&
!isEmpty(wrapperLegacyGroup.encPubkey) &&
!isEmpty(wrapperLegacyGroup.encSeckey)
) {
console.warn('inserting into user wrapper', wrapperLegacyGroup);
await UserGroupsWrapperActions.setLegacyGroup(wrapperLegacyGroup); await UserGroupsWrapperActions.setLegacyGroup(wrapperLegacyGroup);
}
await refreshCachedUserGroup(convoId); await refreshCachedUserGroup(convoId);
} catch (e) { } catch (e) {
window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`); window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`);

@ -377,7 +377,7 @@ export const fetchTopMessagesForConversation = createAsyncThunk(
const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey); const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey);
if (!oldestMessage || oldestMessage.id === oldTopMessageId) { 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; return null;
} }
const messagesProps = await getMessages({ const messagesProps = await getMessages({
@ -414,7 +414,7 @@ export const fetchBottomMessagesForConversation = createAsyncThunk(
const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey); const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey);
if (!mostRecentMessage || mostRecentMessage.id === oldBottomMessageId) { 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; return null;
} }
const messagesProps = await getMessages({ const messagesProps = await getMessages({

@ -2,7 +2,7 @@
// tslint:disable-next-line: no-submodule-imports // tslint:disable-next-line: no-submodule-imports
import { compose } from 'lodash/fp'; import { compose } from 'lodash/fp';
import { escapeRegExp, isRegExp, isString } from 'lodash'; import { escapeRegExp, isNil, isRegExp, isString } from 'lodash';
import { getAppRootPath } from '../node/getRootPath'; import { getAppRootPath } from '../node/getRootPath';
const APP_ROOT_PATH = getAppRootPath(); const APP_ROOT_PATH = getAppRootPath();
@ -98,9 +98,16 @@ const removeNewlines = (text: string) => text.replace(/\r?\n|\r/g, '');
// redactSensitivePaths :: String -> String // redactSensitivePaths :: String -> String
const redactSensitivePaths = redactPath(APP_ROOT_PATH); 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 // redactAll :: String -> String
export const redactAll = !isDev export const redactAll = !shouldNotRedactLogs()
? compose(redactSensitivePaths, redactGroupIds, redactSessionID, redactSnodeIP, redactServerUrl) ? compose(redactSensitivePaths, redactGroupIds, redactSessionID, redactSnodeIP, redactServerUrl)
: (text: string) => text; : (text: string) => text;

Loading…
Cancel
Save