fix: break down deleteContact based on convo type

pull/2756/head
Audric Ackermann 2 years ago
parent 7b42c64cf3
commit 2068737cdd

@ -100,7 +100,7 @@
"glob": "7.1.2", "glob": "7.1.2",
"image-type": "^4.1.0", "image-type": "^4.1.0",
"ip2country": "1.0.1", "ip2country": "1.0.1",
"libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz", "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.16/libsession_util_nodejs-v0.1.16.tar.gz",
"libsodium-wrappers-sumo": "^0.7.9", "libsodium-wrappers-sumo": "^0.7.9",
"linkify-it": "3.0.2", "linkify-it": "3.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",

@ -31,9 +31,10 @@ window.sessionFeatureFlags = {
useTestNet: Boolean( useTestNet: Boolean(
process.env.NODE_APP_INSTANCE && process.env.NODE_APP_INSTANCE.includes('testnet') process.env.NODE_APP_INSTANCE && process.env.NODE_APP_INSTANCE.includes('testnet')
), ),
useDebugLogging: !_.isEmpty(process.env.SESSION_DEBUG),
useClosedGroupV3: false || process.env.USE_CLOSED_GROUP_V3, useClosedGroupV3: false || process.env.USE_CLOSED_GROUP_V3,
debug: { debug: {
debugLogging: !_.isEmpty(process.env.SESSION_DEBUG),
debugLibsessionDumps: !_.isEmpty(process.env.SESSION_DEBUG_LIBSESSION_DUMPS),
debugFileServerRequests: false, debugFileServerRequests: false,
debugNonSnodeRequests: false, debugNonSnodeRequests: false,
debugOnionRequests: false, debugOnionRequests: false,

@ -29,8 +29,9 @@ export const AdminLeaveClosedGroupDialog = (props: { conversationId: string }) =
} }
setLoading(true); 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 // we know want to delete a closed group right after we've left it, so we can call the deleteContact which takes care of it all
await getConversationController().deleteContact(props.conversationId, { await getConversationController().deleteClosedGroup(props.conversationId, {
fromSyncMessage: false, fromSyncMessage: false,
sendLeaveMessage: true,
}); });
setLoading(false); setLoading(false);
closeDialog(); closeDialog();

@ -112,7 +112,7 @@ export const DeletePrivateContactMenuItem = () => {
onClickClose, onClickClose,
okTheme: SessionButtonColor.Danger, okTheme: SessionButtonColor.Danger,
onClickOk: async () => { onClickOk: async () => {
await getConversationController().deleteContact(convoId, { await getConversationController().delete1o1(convoId, {
fromSyncMessage: false, fromSyncMessage: false,
justHidePrivate: false, justHidePrivate: false,
}); });
@ -152,9 +152,16 @@ export const DeleteGroupOrCommunityMenuItem = () => {
onClickClose, onClickClose,
okTheme: SessionButtonColor.Danger, okTheme: SessionButtonColor.Danger,
onClickOk: async () => { onClickOk: async () => {
await getConversationController().deleteContact(convoId, { if (isPublic) {
fromSyncMessage: false, await getConversationController().deleteCommunity(convoId, {
}); fromSyncMessage: false,
});
} else {
await getConversationController().deleteClosedGroup(convoId, {
fromSyncMessage: false,
sendLeaveMessage: true,
});
}
}, },
}) })
); );
@ -450,7 +457,7 @@ export const DeletePrivateConversationMenuItem = () => {
return ( return (
<Item <Item
onClick={async () => { onClick={async () => {
await getConversationController().deleteContact(convoId, { await getConversationController().delete1o1(convoId, {
fromSyncMessage: false, fromSyncMessage: false,
justHidePrivate: true, justHidePrivate: true,
}); });

@ -238,9 +238,10 @@ export function showLeaveGroupByConvoId(conversationId: string) {
UserUtils.getOurPubKeyStrFromCache() UserUtils.getOurPubKeyStrFromCache()
); );
const isClosedGroup = conversation.isClosedGroup() || false; const isClosedGroup = conversation.isClosedGroup() || false;
const isPublic = conversation.isPublic() || false;
// if this is not a closed group, or we are not admin, we can just show a confirmation dialog // if this is a community, or we legacy group are not admin, we can just show a confirmation dialog
if (!isClosedGroup || (isClosedGroup && !isAdmin)) { if (isPublic || (isClosedGroup && !isAdmin)) {
const onClickClose = () => { const onClickClose = () => {
window.inboxStore?.dispatch(updateConfirmModal(null)); window.inboxStore?.dispatch(updateConfirmModal(null));
}; };
@ -249,9 +250,16 @@ export function showLeaveGroupByConvoId(conversationId: string) {
title, title,
message, message,
onClickOk: async () => { onClickOk: async () => {
await getConversationController().deleteContact(conversation.id, { if (isPublic) {
fromSyncMessage: false, await getConversationController().deleteCommunity(conversation.id, {
}); fromSyncMessage: false,
});
} else {
await getConversationController().deleteClosedGroup(conversation.id, {
fromSyncMessage: false,
sendLeaveMessage: true,
});
}
onClickClose(); onClickClose();
}, },
onClickClose, onClickClose,

@ -64,7 +64,7 @@ export async function addKeyPairToCacheAndDBIfNeeded(
return true; return true;
} }
export async function innerRemoveAllClosedGroupEncryptionKeyPairs(groupPubKey: string) { export async function removeAllClosedGroupEncryptionKeyPairs(groupPubKey: string) {
cacheOfClosedGroupKeyPairs.set(groupPubKey, []); cacheOfClosedGroupKeyPairs.set(groupPubKey, []);
await Data.removeAllClosedGroupEncryptionKeyPairs(groupPubKey); await Data.removeAllClosedGroupEncryptionKeyPairs(groupPubKey);
} }
@ -327,26 +327,6 @@ export async function handleNewClosedGroup(
await queueAllCachedFromSource(groupId); await queueAllCachedFromSource(groupId);
} }
/**
*
* @param isKicked if true, we mark the reason for leaving as a we got kicked
*/
export async function markGroupAsLeftOrKicked(
groupPublicKey: string,
groupConvo: ConversationModel,
isKicked: boolean
) {
getSwarmPollingInstance().removePubkey(groupPublicKey);
await innerRemoveAllClosedGroupEncryptionKeyPairs(groupPublicKey);
if (isKicked) {
groupConvo.set('isKickedFromGroup', true);
} else {
groupConvo.set('left', true);
}
}
/** /**
* This function is called when we get a message with the new encryption keypair for a closed group. * This function is called when we get a message with the new encryption keypair for a closed group.
* In this message, we have n-times the same keypair encoded with n being the number of current members. * In this message, we have n-times the same keypair encoded with n being the number of current members.
@ -681,34 +661,39 @@ async function handleClosedGroupMembersRemoved(
// • Stop polling for the group // • Stop polling for the group
// • Remove the key pairs associated with the group // • Remove the key pairs associated with the group
const ourPubKey = UserUtils.getOurPubKeyFromCache(); const ourPubKey = UserUtils.getOurPubKeyFromCache();
const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key);
if (wasCurrentUserRemoved) { if (wasCurrentUserKicked) {
await markGroupAsLeftOrKicked(groupPubKey, convo, true); // we now want to remove everything related to a group when we get kicked from it.
} await getConversationController().deleteClosedGroup(groupPubKey, {
// Note: we don't want to send a new encryption keypair when we get a member removed. fromSyncMessage: false,
// this is only happening when the admin gets a MEMBER_LEFT message sendLeaveMessage: false,
});
// Only add update message if we have something to show } else {
if (membersAfterUpdate.length !== currentMembers.length) { // Note: we don't want to send a new encryption keypair when we get a member removed.
const groupDiff: ClosedGroup.GroupDiff = { // this is only happening when the admin gets a MEMBER_LEFT message
kickedMembers: effectivelyRemovedMembers,
}; // Only add update message if we have something to show
await ClosedGroup.addUpdateMessage( if (membersAfterUpdate.length !== currentMembers.length) {
convo, const groupDiff: ClosedGroup.GroupDiff = {
groupDiff, kickedMembers: effectivelyRemovedMembers,
envelope.senderIdentity, };
_.toNumber(envelope.timestamp) await ClosedGroup.addUpdateMessage(
); convo,
convo.updateLastMessage(); groupDiff,
} envelope.senderIdentity,
_.toNumber(envelope.timestamp)
);
convo.updateLastMessage();
}
// Update the group // Update the group
const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z)); const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z));
convo.set({ members: membersAfterUpdate }); convo.set({ members: membersAfterUpdate });
convo.set({ zombies }); convo.set({ zombies });
await convo.commit(); await convo.commit();
}
await removeFromCache(envelope); await removeFromCache(envelope);
} }
@ -758,56 +743,21 @@ function removeMemberFromZombies(
return true; return true;
} }
async function handleClosedGroupAdminMemberLeft( async function handleClosedGroupAdminMemberLeft(groupPublicKey: string, envelope: EnvelopePlus) {
groupPublicKey: string,
isCurrentUserAdmin: boolean,
convo: ConversationModel,
envelope: EnvelopePlus
) {
// if the admin was remove and we are the admin, it can only be voluntary // if the admin was remove and we are the admin, it can only be voluntary
await markGroupAsLeftOrKicked(groupPublicKey, convo, !isCurrentUserAdmin); await getConversationController().deleteClosedGroup(groupPublicKey, {
fromSyncMessage: false,
// everybody left ! this is how we disable a group when the admin left sendLeaveMessage: false,
const groupDiff: ClosedGroup.GroupDiff = { });
kickedMembers: convo.get('members'),
};
convo.set('members', []);
await ClosedGroup.addUpdateMessage(
convo,
groupDiff,
envelope.senderIdentity,
_.toNumber(envelope.timestamp)
);
convo.updateLastMessage();
await convo.commit();
await removeFromCache(envelope); await removeFromCache(envelope);
} }
async function handleClosedGroupLeftOurself( async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopePlus) {
groupPublicKey: string, // if we ourself left. It can only mean that another of our device left the group and we just synced that message through the swarm
convo: ConversationModel, await getConversationController().deleteClosedGroup(groupId, {
envelope: EnvelopePlus fromSyncMessage: false,
) { sendLeaveMessage: false,
await markGroupAsLeftOrKicked(groupPublicKey, convo, false); });
const groupDiff: ClosedGroup.GroupDiff = {
leavingMembers: [envelope.senderIdentity],
};
await ClosedGroup.addUpdateMessage(
convo,
groupDiff,
envelope.senderIdentity,
_.toNumber(envelope.timestamp)
);
convo.updateLastMessage();
// remove ourself from the list of members
convo.set(
'members',
convo.get('members').filter(m => !UserUtils.isUsFromCache(m))
);
await convo.commit();
await removeFromCache(envelope); await removeFromCache(envelope);
} }
@ -827,18 +777,17 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver
} }
const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
// if the admin leaves, the group is disabled for every members // if the admin leaves, the group is disabled for everyone
const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourPubkey) || false;
if (didAdminLeave) { if (didAdminLeave) {
await handleClosedGroupAdminMemberLeft(groupPublicKey, isCurrentUserAdmin, convo, envelope); await handleClosedGroupAdminMemberLeft(groupPublicKey, envelope);
return; return;
} }
// if we are no longer a member, we LEFT from another device // if we are no longer a member, we LEFT from another device
if (!newMembers.includes(ourPubkey)) { if (!newMembers.includes(ourPubkey)) {
// stop polling, remove all stored pubkeys and make sure the UI does not let us write messages // stop polling, remove everything from this closed group and the corresponding conversation
await handleClosedGroupLeftOurself(groupPublicKey, convo, envelope); await handleClosedGroupLeftOurself(groupPublicKey, envelope);
return; return;
} }

@ -53,8 +53,6 @@ import { queueAllCachedFromSource } from './receiver';
import { EnvelopePlus } from './types'; import { EnvelopePlus } from './types';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../interactions/conversationInteractions'; import { deleteAllMessagesByConvoIdNoConfirmation } from '../interactions/conversationInteractions';
const printDumpsForDebugging = false;
function groupByVariant( function groupByVariant(
incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>> incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>>
) { ) {
@ -99,7 +97,7 @@ async function mergeConfigsWithIncomingUpdates(
data: msg.message.data, data: msg.message.data,
hash: msg.messageHash, hash: msg.messageHash,
})); }));
if (printDumpsForDebugging) { if (window.sessionFeatureFlags.debug.debugLibsessionDumps) {
window.log.info( window.log.info(
`printDumpsForDebugging: before merge of ${variant}:`, `printDumpsForDebugging: before merge of ${variant}:`,
StringUtils.toHex(await GenericWrapperActions.dump(variant)) StringUtils.toHex(await GenericWrapperActions.dump(variant))
@ -125,7 +123,7 @@ async function mergeConfigsWithIncomingUpdates(
`${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} ` `${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} `
); );
if (printDumpsForDebugging) { if (window.sessionFeatureFlags.debug.debugLibsessionDumps) {
window.log.info( window.log.info(
`printDumpsForDebugging: after merge of ${variant}:`, `printDumpsForDebugging: after merge of ${variant}:`,
StringUtils.toHex(await GenericWrapperActions.dump(variant)) StringUtils.toHex(await GenericWrapperActions.dump(variant))
@ -213,7 +211,7 @@ async function deleteContactsFromDB(contactsToRemove: Array<string>) {
for (let index = 0; index < contactsToRemove.length; index++) { for (let index = 0; index < contactsToRemove.length; index++) {
const contactToRemove = contactsToRemove[index]; const contactToRemove = contactsToRemove[index];
try { try {
await getConversationController().deleteContact(contactToRemove, { await getConversationController().delete1o1(contactToRemove, {
fromSyncMessage: true, fromSyncMessage: true,
justHidePrivate: false, justHidePrivate: false,
}); });
@ -360,7 +358,7 @@ async function handleCommunitiesUpdate() {
for (let index = 0; index < communitiesToLeaveInDB.length; index++) { for (let index = 0; index < communitiesToLeaveInDB.length; index++) {
const toLeave = communitiesToLeaveInDB[index]; const toLeave = communitiesToLeaveInDB[index];
window.log.info('leaving community with convoId ', toLeave.id); window.log.info('leaving community with convoId ', toLeave.id);
await getConversationController().deleteContact(toLeave.id, { await getConversationController().deleteCommunity(toLeave.id, {
fromSyncMessage: true, fromSyncMessage: true,
}); });
} }
@ -442,16 +440,11 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
toLeave.id toLeave.id
); );
const toLeaveFromDb = getConversationController().get(toLeave.id); const toLeaveFromDb = getConversationController().get(toLeave.id);
// the wrapper told us that this group is not tracked, so even if we left/got kicked from it, remove it from the DB completely
// if we were kicked from that group, leave it as is until the user manually deletes it await getConversationController().deleteClosedGroup(toLeaveFromDb.id, {
// otherwise, completely remove the conversation fromSyncMessage: true,
if (!toLeaveFromDb?.get('isKickedFromGroup')) { sendLeaveMessage: false, // this comes from the wrapper, so we must have left/got kicked from that group already and our device already handled it.
window.log.debug(`we were kicked from ${toLeave.id} so we keep it until manually deleted`); });
await getConversationController().deleteContact(toLeave.id, {
fromSyncMessage: true,
});
}
} }
for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) { for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) {

@ -82,7 +82,7 @@ async function joinOpenGroupV2(
// we already have a convo associated with it. Remove everything related to it so we start fresh // we already have a convo associated with it. Remove everything related to it so we start fresh
window?.log?.warn('leaving before rejoining open group v2 room', conversationId); window?.log?.warn('leaving before rejoining open group v2 room', conversationId);
await getConversationController().deleteContact(conversationId, { await getConversationController().deleteCommunity(conversationId, {
fromSyncMessage: true, fromSyncMessage: true,
}); });
} }

@ -152,7 +152,7 @@ export class OpenGroupManagerV2 {
await OpenGroupData.removeV2OpenGroupRoom(roomConvoId); await OpenGroupData.removeV2OpenGroupRoom(roomConvoId);
getOpenGroupManager().removeRoomFromPolledRooms(infos); getOpenGroupManager().removeRoomFromPolledRooms(infos);
await getConversationController().deleteContact(roomConvoId, { await getConversationController().deleteCommunity(roomConvoId, {
fromSyncMessage: false, fromSyncMessage: false,
}); });
} }

@ -112,5 +112,5 @@ export function getOpenGroupV2FromConversationId(
* Check if this conversation id corresponds to an OpenGroupV2 conversation. * Check if this conversation id corresponds to an OpenGroupV2 conversation.
*/ */
export function isOpenGroupV2(conversationId: string) { export function isOpenGroupV2(conversationId: string) {
return Boolean(conversationId.startsWith(openGroupPrefix)); return Boolean(conversationId?.startsWith(openGroupPrefix));
} }

@ -109,8 +109,10 @@ export class SwarmPolling {
public removePubkey(pk: PubKey | string) { public removePubkey(pk: PubKey | string) {
const pubkey = PubKey.cast(pk); const pubkey = PubKey.cast(pk);
window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); if (this.groupPolling.some(group => pubkey.key === group.pubkey.key)) {
this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey)); window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key);
this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey));
}
} }
/** /**

@ -22,13 +22,14 @@ import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_uti
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 { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { getMessageQueue } from '..'; import { getMessageQueue } from '..';
import { markGroupAsLeftOrKicked } from '../../receiver/closedGroups';
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 { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
import { isEmpty, isNil } from 'lodash'; import { isEmpty, isNil } from 'lodash';
import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations'; import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups';
import { OpenGroupUtils } from '../apis/open_group_api/utils';
let instance: ConversationController | null; let instance: ConversationController | null;
@ -204,114 +205,88 @@ export class ConversationController {
await conversation.commit(); await conversation.commit();
} }
public async deleteContact( public async deleteClosedGroup(
id: string, groupId: string,
options: { fromSyncMessage: boolean; justHidePrivate?: boolean } options: { fromSyncMessage: boolean; sendLeaveMessage: boolean }
) { ) {
if (!this._initialFetchComplete) { const conversation = await this.deleteConvoInitialChecks(groupId, 'LegacyGroup');
throw new Error('getConversationController.deleteContact needs complete initial fetch'); if (!conversation || !conversation.isClosedGroup()) {
return;
} }
window.log.info(`deleteClosedGroup: ${groupId}, sendLeaveMessage?:${options.sendLeaveMessage}`);
getSwarmPollingInstance().removePubkey(groupId); // we don't need to keep polling anymore.
window.log.info(`deleteContact with ${id}`); if (options.sendLeaveMessage) {
await leaveClosedGroup(groupId, options.fromSyncMessage);
}
const conversation = this.conversations.get(id); // if we were kicked or sent our left message, we have nothing to do more with that group.
if (!conversation) { // Just delete everything related to it, not trying to add update message or send a left message.
window.log.warn(`deleteContact no such convo ${id}`); await this.removeGroupOrCommunityFromDBAndRedux(groupId);
return; await removeLegacyGroupFromWrappers(groupId);
if (!options.fromSyncMessage) {
await ConfigurationSync.queueNewJobIfNeeded();
} }
}
// those are the stuff to do for all conversation types public async deleteCommunity(convoId: string, options: { fromSyncMessage: boolean }) {
window.log.info(`deleteContact destroyingMessages: ${id}`); const conversation = await this.deleteConvoInitialChecks(convoId, 'Community');
await deleteAllMessagesByConvoIdNoConfirmation(id); if (!conversation || !conversation.isPublic()) {
window.log.info(`deleteContact messages destroyed: ${id}`); return;
}
const convoType: ConvoVolatileType = conversation.isClosedGroup()
? 'LegacyGroup'
: conversation.isPublic()
? 'Community'
: '1o1';
switch (convoType) {
case '1o1':
// if this conversation is a private conversation it's in fact a `contact` for desktop.
if (options.justHidePrivate || isNil(options.justHidePrivate) || conversation.isMe()) {
// we just set the hidden field to true
// so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation.
// We also keep the messages for now, as turning a contact as hidden might just be a temporary thing
window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`);
conversation.set({
priority: CONVERSATION_PRIORITIES.hidden,
});
// We don't remove entries from the contacts wrapper, so better keep corresponding convo volatile info for now (it will be pruned if needed)
await conversation.commit(); // this updates the wrappers content to reflect the hidden state
} else {
window.log.info(`deleteContact isPrivate, reset fields and removing from wrapper: ${id}`);
await conversation.setIsApproved(false, false);
await conversation.setDidApproveMe(false, false);
conversation.set('active_at', 0);
await BlockedNumberController.unblockAll([conversation.id]);
await conversation.commit(); // first commit to DB so the DB knows about the changes
if (SessionUtilContact.isContactToStoreInWrapper(conversation)) {
window.log.warn('isContactToStoreInWrapper still true for ', conversation.attributes);
}
if (conversation.id.startsWith('05')) {
// make sure to filter blinded contacts as it will throw otherwise
await SessionUtilContact.removeContactFromWrapper(conversation.id); // then remove the entry alltogether from the wrapper
await SessionUtilConvoInfoVolatile.removeContactFromWrapper(conversation.id);
}
if (getCurrentlySelectedConversationOutsideRedux() === conversation.id) {
window.inboxStore?.dispatch(resetConversationExternal());
}
}
break; window?.log?.info('leaving community: ', conversation.id);
case 'Community': const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversation.id);
window?.log?.info('leaving open group v2', conversation.id); if (roomInfos) {
getOpenGroupManager().removeRoomFromPolledRooms(roomInfos);
try { }
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(conversation.id); await removeCommunityFromWrappers(conversation.id); // this call needs to fetch the pubkey
if (fromWrapper?.fullUrlWithPubkey) { await this.removeGroupOrCommunityFromDBAndRedux(conversation.id);
await SessionUtilConvoInfoVolatile.removeCommunityFromWrapper(
conversation.id,
fromWrapper.fullUrlWithPubkey
);
}
} catch (e) {
window?.log?.info(
'SessionUtilConvoInfoVolatile.removeCommunityFromWrapper failed:',
e.message
);
}
// remove from the wrapper the entries before we remove the roomInfos, as we won't have the required community pubkey afterwards if (!options.fromSyncMessage) {
try { await ConfigurationSync.queueNewJobIfNeeded();
await SessionUtilUserGroups.removeCommunityFromWrapper(conversation.id, conversation.id); }
} catch (e) { }
window?.log?.info('SessionUtilUserGroups.removeCommunityFromWrapper failed:', e);
}
const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversation.id); public async delete1o1(
if (roomInfos) { id: string,
getOpenGroupManager().removeRoomFromPolledRooms(roomInfos); options: { fromSyncMessage: boolean; justHidePrivate?: boolean }
} ) {
await this.cleanUpGroupConversation(conversation.id); const conversation = await this.deleteConvoInitialChecks(id, '1o1');
if (!conversation || !conversation.isPrivate()) {
return;
}
// remove the roomInfos locally for this open group room including the pubkey if (options.justHidePrivate || isNil(options.justHidePrivate) || conversation.isMe()) {
try { // we just set the hidden field to true
await OpenGroupData.removeV2OpenGroupRoom(conversation.id); // so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation.
} catch (e) { // We also keep the messages for now, as turning a contact as hidden might just be a temporary thing
window?.log?.info('removeV2OpenGroupRoom failed:', e); window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`);
} conversation.set({
break; priority: CONVERSATION_PRIORITIES.hidden,
case 'LegacyGroup': });
window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`); // We don't remove entries from the contacts wrapper, so better keep corresponding convo volatile info for now (it will be pruned if needed)
await leaveClosedGroup(conversation.id, options.fromSyncMessage); // this removes the data from the group and convo volatile info await conversation.commit(); // this updates the wrappers content to reflect the hidden state
await this.cleanUpGroupConversation(conversation.id); } else {
break; window.log.info(`deleteContact isPrivate, reset fields and removing from wrapper: ${id}`);
default:
assertUnreachable(convoType, `deleteContact: convoType ${convoType} not handled`); await conversation.setIsApproved(false, false);
await conversation.setDidApproveMe(false, false);
conversation.set('active_at', 0);
await BlockedNumberController.unblockAll([conversation.id]);
await conversation.commit(); // first commit to DB so the DB knows about the changes
if (SessionUtilContact.isContactToStoreInWrapper(conversation)) {
window.log.warn('isContactToStoreInWrapper still true for ', conversation.attributes);
}
if (conversation.id.startsWith('05')) {
// make sure to filter blinded contacts as it will throw otherwise
await SessionUtilContact.removeContactFromWrapper(conversation.id); // then remove the entry alltogether from the wrapper
await SessionUtilConvoInfoVolatile.removeContactFromWrapper(conversation.id);
}
if (getCurrentlySelectedConversationOutsideRedux() === conversation.id) {
window.inboxStore?.dispatch(resetConversationExternal());
}
} }
if (!options.fromSyncMessage) { if (!options.fromSyncMessage) {
@ -415,32 +390,62 @@ export class ConversationController {
this.conversations.reset([]); this.conversations.reset([]);
} }
private async cleanUpGroupConversation(id: string) { private async deleteConvoInitialChecks(convoId: string, deleteType: ConvoVolatileType) {
window.log.info(`deleteContact isGroup, removing convo from DB: ${id}`); if (!this._initialFetchComplete) {
throw new Error(`getConversationController.${deleteType} needs complete initial fetch`);
}
window.log.info(`${deleteType} with ${convoId}`);
const conversation = this.conversations.get(convoId);
if (!conversation) {
window.log.warn(`${deleteType} no such convo ${convoId}`);
return null;
}
// those are the stuff to do for all conversation types
window.log.info(`${deleteType} destroyingMessages: ${convoId}`);
await deleteAllMessagesByConvoIdNoConfirmation(convoId);
window.log.info(`${deleteType} messages destroyed: ${convoId}`);
return conversation;
}
private async removeGroupOrCommunityFromDBAndRedux(convoId: string) {
window.log.info(`cleanUpGroupConversation, removing convo from DB: ${convoId}`);
// not a private conversation, so not a contact for the ContactWrapper // not a private conversation, so not a contact for the ContactWrapper
await Data.removeConversation(id); await Data.removeConversation(convoId);
window.log.info(`deleteContact isGroup, convo removed from DB: ${id}`); // remove the data from the opengrouprooms table too if needed
const conversation = this.conversations.get(id); if (convoId && OpenGroupUtils.isOpenGroupV2(convoId)) {
// remove the roomInfos locally for this open group room including the pubkey
try {
await OpenGroupData.removeV2OpenGroupRoom(convoId);
} catch (e) {
window?.log?.info('removeV2OpenGroupRoom failed:', e);
}
}
window.log.info(`cleanUpGroupConversation, convo removed from DB: ${convoId}`);
const conversation = this.conversations.get(convoId);
if (conversation) { if (conversation) {
this.conversations.remove(conversation); this.conversations.remove(conversation);
window?.inboxStore?.dispatch( window?.inboxStore?.dispatch(
conversationActions.conversationChanged({ conversationActions.conversationChanged({
id: id, id: convoId,
data: conversation.getConversationModelProps(), data: conversation.getConversationModelProps(),
}) })
); );
} }
window.inboxStore?.dispatch(conversationActions.conversationRemoved(id)); window.inboxStore?.dispatch(conversationActions.conversationRemoved(convoId));
window.log.info(`deleteContact NOT private, convo removed from store: ${id}`); window.log.info(`cleanUpGroupConversation, convo removed from store: ${convoId}`);
} }
} }
/** /**
* You most likely don't want to call this function directly, but instead use the deleteContact() from the ConversationController as it will take care of more cleaningup. * You most likely don't want to call this function directly, but instead use the deleteLegacyGroup() from the ConversationController as it will take care of more cleaningup.
* *
* Note: `fromSyncMessage` is used to know if we need to send a leave group message to the group first. * 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. * So if the user made the action on this device, fromSyncMessage should be false, but if it happened from a linked device polled update, set this to true.
@ -475,20 +480,12 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
await convo.updateGroupAdmins(admins, false); await convo.updateGroupAdmins(admins, false);
await convo.commit(); await convo.commit();
const source = UserUtils.getOurPubKeyStrFromCache();
const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset(); const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset();
const dbMessage = await convo.addSingleOutgoingMessage({
group_update: { left: [source] },
sent_at: networkTimestamp,
expireTimer: 0,
});
getSwarmPollingInstance().removePubkey(groupId); getSwarmPollingInstance().removePubkey(groupId);
if (fromSyncMessage) { if (fromSyncMessage) {
// no need to send our leave message as our other device should already have sent it. // no need to send our leave message as our other device should already have sent it.
await cleanUpFullyLeftLegacyGroup(groupId);
return; return;
} }
@ -496,7 +493,6 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
if (!keypair || isEmpty(keypair) || isEmpty(keypair.publicHex) || isEmpty(keypair.privateHex)) { if (!keypair || isEmpty(keypair) || isEmpty(keypair.publicHex) || isEmpty(keypair.privateHex)) {
// if we do not have a keypair, we won't be able to send our leaving message neither, so just skip sending it. // if we do not have a keypair, we won't be able to send our leaving message neither, so just skip sending it.
// this can happen when getting a group from a broken libsession usergroup wrapper, but not only. // this can happen when getting a group from a broken libsession usergroup wrapper, but not only.
await cleanUpFullyLeftLegacyGroup(groupId);
return; return;
} }
@ -504,7 +500,6 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ const ourLeavingMessage = new ClosedGroupMemberLeftMessage({
timestamp: networkTimestamp, timestamp: networkTimestamp,
groupId, groupId,
identifier: dbMessage.id as string,
}); });
window?.log?.info(`We are leaving the group ${groupId}. Sending our leaving message.`); window?.log?.info(`We are leaving the group ${groupId}. Sending our leaving message.`);
@ -521,17 +516,42 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) {
window?.log?.info( window?.log?.info(
`Leaving message sent ${groupId}. Removing everything related to this group.` `Leaving message sent ${groupId}. Removing everything related to this group.`
); );
await cleanUpFullyLeftLegacyGroup(groupId); } else {
window?.log?.info(
`Leaving message failed to be sent for ${groupId}. But still removing everything related to this group....`
);
} }
// if we failed to send our leaving message, don't remove everything yet as we might want to retry sending our leaving message later. // the rest of the cleaning of that conversation is done in the `deleteClosedGroup()`
} }
async function cleanUpFullyLeftLegacyGroup(groupId: string) { async function removeLegacyGroupFromWrappers(groupId: string) {
const convo = getConversationController().get(groupId); getSwarmPollingInstance().removePubkey(groupId);
await UserGroupsWrapperActions.eraseLegacyGroup(groupId); await UserGroupsWrapperActions.eraseLegacyGroup(groupId);
await SessionUtilConvoInfoVolatile.removeLegacyGroupFromWrapper(groupId); await SessionUtilConvoInfoVolatile.removeLegacyGroupFromWrapper(groupId);
if (convo) { await removeAllClosedGroupEncryptionKeyPairs(groupId);
await markGroupAsLeftOrKicked(groupId, convo, false); }
async function removeCommunityFromWrappers(conversationId: string) {
if (!conversationId || !OpenGroupUtils.isOpenGroupV2(conversationId)) {
return;
}
try {
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(conversationId);
if (fromWrapper?.fullUrlWithPubkey) {
await SessionUtilConvoInfoVolatile.removeCommunityFromWrapper(
conversationId,
fromWrapper.fullUrlWithPubkey
);
}
} catch (e) {
window?.log?.info('SessionUtilConvoInfoVolatile.removeCommunityFromWrapper failed:', e.message);
}
// remove from the wrapper the entries before we remove the roomInfos, as we won't have the required community pubkey afterwards
try {
await SessionUtilUserGroups.removeCommunityFromWrapper(conversationId, conversationId);
} catch (e) {
window?.log?.info('SessionUtilUserGroups.removeCommunityFromWrapper failed:', e.message);
} }
} }

@ -404,6 +404,7 @@ async function sendMessagesToSnode(
retries: 2, retries: 2,
factor: 1, factor: 1,
minTimeout: MessageSender.getMinRetryTimeout(), minTimeout: MessageSender.getMinRetryTimeout(),
maxTimeout: 1000,
} }
); );

@ -238,7 +238,7 @@ async function _runJob(job: any) {
if (currentAttempt >= 3 || was404Error(error)) { if (currentAttempt >= 3 || was404Error(error)) {
logger.error( logger.error(
`_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${found?.idForLogging()} as permanent error:`, `_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${found?.idForLogging()} as permanent error:`,
error && error.stack ? error.stack : error error && error.message ? error.message : error
); );
// Make sure to fetch the message from DB here right before writing it. // Make sure to fetch the message from DB here right before writing it.

@ -19,8 +19,8 @@ import {
import { ReleasedFeatures } from '../../../../util/releaseFeature'; import { ReleasedFeatures } from '../../../../util/releaseFeature';
import { allowOnlyOneAtATime } from '../../Promise'; import { allowOnlyOneAtATime } from '../../Promise';
const defaultMsBetweenRetries = 3000; const defaultMsBetweenRetries = 30000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s)
const defaultMaxAttempts = 3; const defaultMaxAttempts = 2;
/** /**
* We want to run each of those jobs at least 3seconds apart. * We want to run each of those jobs at least 3seconds apart.

@ -110,7 +110,7 @@ const development = window && window?.getEnvironment && window?.getEnvironment()
// The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api // The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api
function logAtLevel(level: string, prefix: string, ...args: any) { function logAtLevel(level: string, prefix: string, ...args: any) {
if (prefix === 'DEBUG' && !window.sessionFeatureFlags.useDebugLogging) { if (prefix === 'DEBUG' && !window.sessionFeatureFlags.debug.debugLogging) {
return; return;
} }
if (development) { if (development) {

@ -1,8 +1,8 @@
/* eslint-env node */ /* eslint-env node */
// tslint:disable-next-line: no-submodule-imports // tslint:disable-next-line: no-submodule-imports
import { escapeRegExp, isEmpty, isRegExp, isString } from 'lodash';
import { compose } from 'lodash/fp'; import { compose } from 'lodash/fp';
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();
@ -99,9 +99,9 @@ const removeNewlines = (text: string) => text.replace(/\r?\n|\r/g, '');
const redactSensitivePaths = redactPath(APP_ROOT_PATH); const redactSensitivePaths = redactPath(APP_ROOT_PATH);
function shouldNotRedactLogs() { function shouldNotRedactLogs() {
// if the env variable `SESSION_NO_REDACT` is set, trust it as a boolean // if featureFlag is set to true, trust it
if (!isNil(process.env.SESSION_NO_REDACT)) { if (!isEmpty(process.env.SESSION_DEBUG_DISABLE_REDACTED)) {
return process.env.SESSION_NO_REDACT; return true;
} }
// otherwise we don't want to redact logs when running on the devprod env // otherwise we don't want to redact logs when running on the devprod env
return (process.env.NODE_APP_INSTANCE || '').startsWith('devprod'); return (process.env.NODE_APP_INSTANCE || '').startsWith('devprod');

3
ts/window.d.ts vendored

@ -38,11 +38,12 @@ declare global {
useTestNet: boolean; useTestNet: boolean;
useClosedGroupV3: boolean; useClosedGroupV3: boolean;
debug: { debug: {
debugLogging: boolean;
debugLibsessionDumps: boolean;
debugFileServerRequests: boolean; debugFileServerRequests: boolean;
debugNonSnodeRequests: boolean; debugNonSnodeRequests: boolean;
debugOnionRequests: boolean; debugOnionRequests: boolean;
}; };
useDebugLogging: boolean;
}; };
SessionSnodeAPI: SessionSnodeAPI; SessionSnodeAPI: SessionSnodeAPI;
onLogin: (pw: string) => Promise<void>; onLogin: (pw: string) => Promise<void>;

@ -5148,9 +5148,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz": "libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.16/libsession_util_nodejs-v0.1.16.tar.gz":
version "0.1.15" version "0.1.16"
resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz#276b878bbd68261009dd1081b97e25ee6769fd62" resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.16/libsession_util_nodejs-v0.1.16.tar.gz#2a526154b7d0f4235895f3a788704a56d6573339"
dependencies: dependencies:
cmake-js "^7.2.1" cmake-js "^7.2.1"
node-addon-api "^6.1.0" node-addon-api "^6.1.0"

Loading…
Cancel
Save