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 { 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 (
<SessionWrapperModal title={titleText} onClose={closeDialog}>
<SpacerLG />
<StyledWarning>{warningAsAdmin}</StyledWarning>
<StyledWarning>{window.i18n('leaveGroupConfirmationAdmin')}</StyledWarning>
<SessionSpinner loading={loading} />
<div className="session-modal__button-group">
<SessionButton
text={okText}
text={window.i18n('leaveAndRemoveForEveryone')}
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
onClick={onClickOK}
/>
<SessionButton
text={cancelText}
text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple}
onClick={closeDialog}
/>

@ -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 }));

@ -447,8 +447,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
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';
}

@ -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) {

@ -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;
}

@ -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);

@ -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<Promise<any>> = [];
@ -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) {

@ -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<RetrieveMessageItem>) {

@ -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<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 { 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<string
const us = UserUtils.getOurPubKeyStrFromCache();
const identityKeyPair = await generateGroupV3Keypair();
if (!identityKeyPair) {
throw new Error('Could not create identity keypair for new closed group v3');
}
// const identityKeyPair = await generateGroupV3Keypair();
// if (!identityKeyPair) {
// 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
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
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");
throw new Error("Couldn't get key pair for closed group");
}
const hexPubFromECKeyPair = PubKey.cast(hexEncryptionKeyPair.publicHex);
const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol(

@ -2,34 +2,30 @@ import { PubKey } from '../types';
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 { SignalService } from '../../protobuf';
import { generateCurve25519KeyPairWithoutPrefix } from '../crypto';
import { encryptUsingSessionProtocol } from '../crypto/MessageEncrypter';
import { ECKeyPair } from '../../receiver/keypairs';
import { UserUtils } from '../utils';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { getMessageQueue } from '..';
import { Data } from '../../data/data';
import { ConversationModel } from '../../models/conversation';
import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes';
import { MessageModel } from '../../models/message';
import { SignalService } from '../../protobuf';
import {
addKeyPairToCacheAndDBIfNeeded,
distributingClosedGroupEncryptionKeyPairs,
markGroupAsLeftOrKicked,
} 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 { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage';
import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage';
import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage';
import { getSwarmPollingInstance } from '../apis/snode_api';
import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { UserUtils } from '../utils';
import { fromHexToArray, toHex } from '../utils/String';
export type GroupInfo = {
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) {
if (name.length === 0) {
window?.log?.warn('No name given for group update. Skipping');

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

@ -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}`);

@ -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({

@ -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;

Loading…
Cancel
Save