diff --git a/preload.js b/preload.js index 14050ccf2..72f1e600c 100644 --- a/preload.js +++ b/preload.js @@ -35,6 +35,7 @@ window.sessionFeatureFlags = { debug: { debugLogging: !_.isEmpty(process.env.SESSION_DEBUG), debugLibsessionDumps: !_.isEmpty(process.env.SESSION_DEBUG_LIBSESSION_DUMPS), + debugBuiltSnodeRequests: !_.isEmpty(process.env.SESSION_DEBUG_BUILT_SNODE_REQUEST), debugFileServerRequests: false, debugNonSnodeRequests: false, debugOnionRequests: false, diff --git a/ts/data/sharedDataTypes.ts b/ts/data/sharedDataTypes.ts index 881a0c486..2f9c92e3c 100644 --- a/ts/data/sharedDataTypes.ts +++ b/ts/data/sharedDataTypes.ts @@ -1,7 +1,5 @@ import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; -type PrArrayMsgIds = Promise>; - export type DataCallArgs any> = Parameters[0]; export type DeleteAllMessageFromSendersInConversationType = ( @@ -9,14 +7,14 @@ export type DeleteAllMessageFromSendersInConversationType = ( toRemove: Array; signatureTimestamp: number; } -) => PrArrayMsgIds; +) => Promise<{ messageHashes: Array }>; export type DeleteAllMessageHashesInConversationType = ( args: WithGroupPubkey & { messageHashes: Array; signatureTimestamp: number; } -) => PrArrayMsgIds; +) => Promise<{ messageHashes: Array }>; export type DeleteAllMessageHashesInConversationMatchingAuthorType = ( args: WithGroupPubkey & { diff --git a/ts/interactions/conversations/unsendingInteractions.ts b/ts/interactions/conversations/unsendingInteractions.ts index e51a3e3fd..7e982c4c4 100644 --- a/ts/interactions/conversations/unsendingInteractions.ts +++ b/ts/interactions/conversations/unsendingInteractions.ts @@ -188,6 +188,7 @@ export async function deleteMessagesFromSwarmOnly( pubkey: PubkeyType | GroupPubkeyType ) { const deletionMessageHashes = isStringArray(messages) ? messages : getMessageHashes(messages); + try { if (isEmpty(messages)) { return false; diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 5a886cb66..2d308d800 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -1095,14 +1095,17 @@ function deleteAllMessageFromSendersInConversation( instance?: BetterSqlite3.Database ): AwaitedReturn { if (!groupPk || !toRemove.length) { - return []; + return { messageHashes: [] }; } - return assertGlobalInstanceOrInstance(instance) - .prepare( - `DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND source IN ( ${toRemove.map(() => '?').join(', ')} ) RETURNING id` - ) - .all(groupPk, signatureTimestamp, ...toRemove) - .map(m => m.id); + const messageHashes = compact( + assertGlobalInstanceOrInstance(instance) + .prepare( + `DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND source IN ( ${toRemove.map(() => '?').join(', ')} ) RETURNING messageHash` + ) + .all(groupPk, signatureTimestamp, ...toRemove) + .map(m => m.messageHash) + ); + return { messageHashes }; } function deleteAllMessageHashesInConversation( @@ -1114,14 +1117,17 @@ function deleteAllMessageHashesInConversation( instance?: BetterSqlite3.Database ): AwaitedReturn { if (!groupPk || !messageHashes.length) { - return []; + return { messageHashes: [] }; } - return assertGlobalInstanceOrInstance(instance) - .prepare( - `DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id` - ) - .all(groupPk, signatureTimestamp, ...messageHashes) - .map(m => m.id); + const deletedMessageHashes = compact( + assertGlobalInstanceOrInstance(instance) + .prepare( + `DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING messageHash` + ) + .all(groupPk, signatureTimestamp, ...messageHashes) + .map(m => m.messageHash) + ); + return { messageHashes: deletedMessageHashes }; } function deleteAllMessageHashesInConversationMatchingAuthor( diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts index 537f241b7..bbb05a910 100644 --- a/ts/receiver/groupv2/handleGroupV2Message.ts +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -1,5 +1,5 @@ import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; -import { isEmpty, isFinite, isNumber } from 'lodash'; +import { compact, isEmpty, isFinite, isNumber } from 'lodash'; import { Data } from '../../data/data'; import { deleteMessagesFromSwarmOnly } from '../../interactions/conversations/unsendingInteractions'; import { ConversationTypeEnum } from '../../models/conversationAttributes'; @@ -21,7 +21,7 @@ import { PreConditionFailed } from '../../session/utils/errors'; import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils'; import { SessionUtilConvoInfoVolatile } from '../../session/utils/libsession/libsession_utils_convo_info_volatile'; -import { messagesExpired } from '../../state/ducks/conversations'; +import { messageHashesExpired, messagesExpired } from '../../state/ducks/conversations'; import { groupInfoActions } from '../../state/ducks/metaGroups'; import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; import { BlockedNumberController } from '../../util'; @@ -460,10 +460,10 @@ async function handleGroupDeleteMemberContentMessage({ }); // this is step 3. window.inboxStore.dispatch( - messagesExpired( - [...deletedByHashes, ...deletedBySenders].map(m => ({ + messageHashesExpired( + compact([...deletedByHashes.messageHashes, ...deletedBySenders.messageHashes]).map(m => ({ conversationKey: groupPk, - messageId: m, + messageHash: m, })) ) ); diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 8f9bc8367..af8efd559 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -565,7 +565,7 @@ export class SwarmPolling { } if (type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(pubkey)) { const toBump = await MetaGroupWrapperActions.currentHashes(pubkey); - window.log.debug(`configHashesToBump group count: ${toBump.length}`); + window.log.debug(`configHashesToBump group(${ed25519Str(pubkey)}) count: ${toBump.length}`); return toBump; } return []; diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index f45e012fa..78ac7ad1f 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -56,6 +56,7 @@ import { UserUtils } from '../utils'; import { ed25519Str, fromUInt8ArrayToBase64 } from '../utils/String'; import { MessageSentHandler } from './MessageSentHandler'; import { MessageWrapper } from './MessageWrapper'; +import { stringify } from '../../types/sqlSharedTypes'; // ================ SNODE STORE ================ @@ -347,6 +348,19 @@ async function getSignatureParamsFromNamespace( return {}; } +function logBuildSubRequests(subRequests: Array) { + if (!window.sessionFeatureFlags.debug.debugBuiltSnodeRequests) { + return; + } + window.log.debug( + `\n========================================\nsubRequests: [\n\t${subRequests + .map(m => { + return stringify(m); + }) + .join(',\n\t')}]\n========================================` + ); +} + async function signSubRequests( params: Array ): Promise> { @@ -356,6 +370,8 @@ async function signSubRequests( }) ); + logBuildSubRequests(signedRequests); + return signedRequests; } diff --git a/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts b/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts index 796a08dfb..84adaf0af 100644 --- a/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts @@ -5,6 +5,7 @@ import { v4 } from 'uuid'; import { StringUtils } from '../..'; import { Data } from '../../../../data/data'; import { deleteMessagesFromSwarmOnly } from '../../../../interactions/conversations/unsendingInteractions'; +import { messageHashesExpired } from '../../../../state/ducks/conversations'; import { MetaGroupWrapperActions, MultiEncryptWrapperActions, @@ -217,8 +218,21 @@ class GroupPendingRemovalsJob extends PersistedJob ({ + conversationKey: groupPk, + messageHash, + })) + ) + ); + } } } } catch (e) { diff --git a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts index 5dd00b59d..e846513cf 100644 --- a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts @@ -11,6 +11,7 @@ import { } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { DeleteAllFromGroupMsgNodeSubRequest, + DeleteHashesFromGroupNodeSubRequest, StoreGroupKeysSubRequest, StoreGroupMessageSubRequest, SubaccountRevokeSubRequest, @@ -144,27 +145,30 @@ async function pushChangesToGroupSwarmIfNeeded({ m => m instanceof SubaccountRevokeSubRequest || m instanceof SubaccountUnrevokeSubRequest || - m instanceof DeleteAllFromGroupMsgNodeSubRequest + m instanceof DeleteAllFromGroupMsgNodeSubRequest || + m instanceof DeleteHashesFromGroupNodeSubRequest ); + const sortedSubRequests = compact([ + supplementalKeysSubRequest, // this needs to be stored first + ...pendingConfigRequests, // groupKeys are first in this array, so all good, then groupInfos are next + ...extraStoreRequests, // this can be stored anytime + ...extraRequests, + ]); + const result = await MessageSender.sendEncryptedDataToSnode({ // Note: this is on purpose that supplementalKeysSubRequest is before pendingConfigRequests // as this is to avoid a race condition where a device is polling right // while we are posting the configs (already encrypted with the new keys) - sortedSubRequests: compact([ - supplementalKeysSubRequest, // this needs to be stored first - ...pendingConfigRequests, // groupKeys are first in this array, so all good, then groupInfos are next - ...extraStoreRequests, // this can be stored anytime - ...extraRequests, - ]), + sortedSubRequests, destination: groupPk, method: 'sequence', }); const expectedReplyLength = - pendingConfigRequests.length + // each of those are sent as a subrequest (supplementalKeysSubRequest ? 1 : 0) + // we are sending all the supplemental keys as a single subrequest - (extraStoreRequests ? 1 : 0) + // each of those are sent as a subrequest + pendingConfigRequests.length + // each of those are sent as a subrequest + extraStoreRequests.length + // each of those are sent as a subrequest extraRequestWithExpectedResults.length; // each of those are sent as a subrequest, but they don't all return something... // we do a sequence call here. If we do not have the right expected number of results, consider it a failure diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index a193daf8c..0c9dbad63 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -601,18 +601,32 @@ function handleMessagesChangedOrAdded( function handleMessageExpiredOrDeleted( state: ConversationsStateType, - payload: { - messageId: string; - conversationKey: string; - } + payload: { conversationKey: string } & ( + | { + messageId: string; + } + | { + messageHash: string; + } + ) ) { - const { conversationKey, messageId } = payload; + const { conversationKey } = payload; + const messageId = (payload as any).messageId as string | undefined; + const messageHash = (payload as any).messageHash as string | undefined; + if (conversationKey === state.selectedConversation) { // search if we find this message id. // we might have not loaded yet, so this case might not happen - const messageInStoreIndex = state?.messages.findIndex(m => m.propsForMessage.id === messageId); + const messageInStoreIndex = state?.messages.findIndex( + m => + (messageId && m.propsForMessage.id === messageId) || + (messageHash && m.propsForMessage.messageHash === messageHash) + ); const editedQuotes = { ...state.quotes }; if (messageInStoreIndex >= 0) { + const msgToRemove = state.messages[messageInStoreIndex]; + const extractedMessageId = msgToRemove.propsForMessage.id; + // we cannot edit the array directly, so slice the first part, and slice the second part, // keeping the index removed out const editedMessages = [ @@ -637,7 +651,9 @@ function handleMessageExpiredOrDeleted( messages: editedMessages, quotes: editedQuotes, firstUnreadMessageId: - state.firstUnreadMessageId === messageId ? undefined : state.firstUnreadMessageId, + state.firstUnreadMessageId === extractedMessageId + ? undefined + : state.firstUnreadMessageId, }; } @@ -649,10 +665,16 @@ function handleMessageExpiredOrDeleted( function handleMessagesExpiredOrDeleted( state: ConversationsStateType, action: PayloadAction< - Array<{ - messageId: string; - conversationKey: string; - }> + Array< + { conversationKey: string } & ( + | { + messageId: string; + } + | { + messageHash: string; + } + ) + > > ): ConversationsStateType { let stateCopy = state; @@ -797,6 +819,17 @@ const conversationsSlice = createSlice({ ) { return handleMessagesExpiredOrDeleted(state, action); }, + messageHashesExpired( + state: ConversationsStateType, + action: PayloadAction< + Array<{ + messageHash: string; + conversationKey: string; + }> + > + ) { + return handleMessagesExpiredOrDeleted(state, action); + }, messagesDeleted( state: ConversationsStateType, @@ -1139,6 +1172,7 @@ export const { conversationRemoved, removeAllConversations, messagesExpired, + messageHashesExpired, messagesDeleted, conversationReset, messagesChanged, diff --git a/ts/types/sqlSharedTypes.ts b/ts/types/sqlSharedTypes.ts index 36b8a7c6c..413ff8f4a 100644 --- a/ts/types/sqlSharedTypes.ts +++ b/ts/types/sqlSharedTypes.ts @@ -308,11 +308,15 @@ export function toFixedUint8ArrayOfLength( } export function stringify(obj: unknown) { - return JSON.stringify(obj, (_key, value) => { - return value instanceof Uint8Array - ? `Uint8Array(${value.length}): ${toHex(value)}` - : value?.type === 'Buffer' && value?.data - ? `Buffer: ${toHex(value.data)}` - : value; - }); + return JSON.stringify( + obj, + (_key, value) => { + return value instanceof Uint8Array + ? `Uint8Array(${value.length}): ${toHex(value)}` + : value?.type === 'Buffer' && value?.data + ? `Buffer: ${toHex(value.data)}` + : value; + }, + 2 + ); } diff --git a/ts/window.d.ts b/ts/window.d.ts index 06b052c3c..9a7dc27d9 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -34,6 +34,7 @@ declare global { debug: { debugLogging: boolean; debugLibsessionDumps: boolean; + debugBuiltSnodeRequests: boolean; debugFileServerRequests: boolean; debugNonSnodeRequests: boolean; debugOnionRequests: boolean;