From 6ae0e3aac9739969feca4f5235ae50b0f3127faa Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 13 Jan 2025 14:12:28 +1100 Subject: [PATCH] fix: reset lasthash on promotion --- ts/interactions/conversationInteractions.ts | 1 + ts/node/sql.ts | 15 +++++++++++-- ts/receiver/closedGroups.ts | 4 ++++ ts/receiver/groupv2/handleGroupV2Message.ts | 15 +++++++++++-- ts/receiver/queuedJob.ts | 1 + ts/session/apis/snode_api/swarmPolling.ts | 21 +++++++++++++++++++ .../conversations/ConversationController.ts | 1 + ts/session/group/closed-group.ts | 8 ++++++- ts/session/types/with.ts | 1 + ts/state/ducks/metaGroups.ts | 6 ++++++ 10 files changed, 68 insertions(+), 5 deletions(-) diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 3fcb8326b..583d3c382 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -1010,6 +1010,7 @@ export async function promoteUsersInGroup({ sentAt, convo, markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier + messageHash: null, }); const groupMemberChange = await GroupUpdateMessageFactory.getPromotedControlMessage({ adminSecretKey: groupInWrapper.secretKey, diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 554b23e36..004887970 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -1769,13 +1769,24 @@ function getLastHashBySnode(convoId: string, snode: string, namespace: number) { } function getSeenMessagesByHashList(hashes: Array) { - const rows = assertGlobalInstance() + const fromSeenTableRows = assertGlobalInstance() .prepare( `SELECT * FROM ${SEEN_MESSAGE_TABLE} WHERE hash IN ( ${hashes.map(() => '?').join(', ')} );` ) .all(hashes); - return map(rows, row => row.hash); + const fromMessagesTableRows = compact( + assertGlobalInstance() + .prepare( + `SELECT messageHash FROM ${MESSAGES_TABLE} WHERE messageHash IN ( ${hashes.map(() => '?').join(', ')} )` + ) + .all(hashes) + ); + + const hashesFromSeen: Array = map(fromSeenTableRows, row => row.hash); + const hashesFromMessages: Array = map(fromMessagesTableRows, row => row.messageHash); + + return uniq(hashesFromSeen.concat(hashesFromMessages)); } function getExpiredMessages() { diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 8324f09bb..d4929b853 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -635,6 +635,7 @@ async function handleClosedGroupNameChanged( sentAt: toNumber(envelope.timestamp), expireUpdate, markAlreadySent: false, // legacy groups support will be removed eventually + messageHash: null, // legacy groups }); if (!shouldOnlyAddUpdateMessage) { convo.set({ displayNameInProfile: newName }); @@ -699,6 +700,7 @@ async function handleClosedGroupMembersAdded( sentAt: toNumber(envelope.timestamp), expireUpdate, markAlreadySent: false, // legacy groups support will be removed eventually + messageHash: null, // legacy groups }); if (!shouldOnlyAddUpdateMessage) { @@ -793,6 +795,7 @@ async function handleClosedGroupMembersRemoved( sentAt: toNumber(envelope.timestamp), expireUpdate, markAlreadySent: false, // legacy groups support will be removed eventually + messageHash: null, // legacy groups }); convo.updateLastMessage(); } @@ -932,6 +935,7 @@ async function handleClosedGroupMemberLeft( sentAt: toNumber(envelope.timestamp), expireUpdate, markAlreadySent: false, // legacy groups support will be removed eventually + messageHash: null, // legacy groups }); convo.updateLastMessage(); // if a user just left and we are the admin, we remove him right away for everyone by sending a MEMBERS_REMOVED message so no need to add him as a zombie diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts index 6c6ec0b0d..2db57144b 100644 --- a/ts/receiver/groupv2/handleGroupV2Message.ts +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -12,7 +12,7 @@ import { getSodiumRenderer } from '../../session/crypto'; import { WithDisappearingMessageUpdate } from '../../session/disappearing_messages/types'; import { ClosedGroup } from '../../session/group/closed-group'; import { PubKey } from '../../session/types'; -import { WithMessageHash } from '../../session/types/with'; +import { WithMessageHash, type WithMessageHashOrNull } from '../../session/types/with'; import { UserUtils } from '../../session/utils'; import { sleepFor } from '../../session/utils/Promise'; import { ed25519Str, stringToUint8Array } from '../../session/utils/String'; @@ -44,7 +44,8 @@ type GroupUpdateGeneric = { } & WithSignatureTimestamp & WithGroupPubkey & WithAuthor & - WithDisappearingMessageUpdate; + WithDisappearingMessageUpdate & + WithMessageHashOrNull; type GroupUpdateDetails = { updateMessage: SignalService.GroupUpdateMessage; @@ -226,6 +227,7 @@ async function handleGroupInfoChangeMessage({ signatureTimestamp, author, expireUpdate, + messageHash, }: GroupUpdateGeneric) { const sigValid = await verifySig({ pubKey: HexString.fromHexStringNoPrefix(groupPk), @@ -252,6 +254,7 @@ async function handleGroupInfoChangeMessage({ sentAt: signatureTimestamp, expireUpdate, markAlreadySent: true, + messageHash, }); break; @@ -264,6 +267,7 @@ async function handleGroupInfoChangeMessage({ sentAt: signatureTimestamp, expireUpdate, markAlreadySent: true, + messageHash, }); break; } @@ -298,6 +302,7 @@ async function handleGroupMemberChangeMessage({ signatureTimestamp, author, expireUpdate, + messageHash, }: GroupUpdateGeneric) { const convo = ConvoHub.use().get(groupPk); if (!convo) { @@ -327,6 +332,7 @@ async function handleGroupMemberChangeMessage({ sentAt: signatureTimestamp, expireUpdate, markAlreadySent: true, + messageHash, }; switch (change.type) { @@ -386,6 +392,7 @@ async function handleGroupUpdateMemberLeftNotificationMessage({ signatureTimestamp, author, expireUpdate, + messageHash, }: GroupUpdateGeneric) { // No need to verify sig, the author is already verified with the libsession.decrypt() const convo = ConvoHub.use().get(groupPk); @@ -401,6 +408,7 @@ async function handleGroupUpdateMemberLeftNotificationMessage({ sentAt: signatureTimestamp, expireUpdate, markAlreadySent: true, + messageHash, }); convo.set({ @@ -619,6 +627,9 @@ async function handleGroupUpdatePromoteMessage({ await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache()); if (!found.invitePending) { + // yes, we really want to refetch the whole history of messages from that group... + await ConvoHub.use().resetLastHashesForConversation(groupPk); + // This group should already be polling based on if that author is pre-approved or we've already approved that group from another device. // Start polling from it, we will mark ourselves as admin once we get the first merge result, if needed. getSwarmPollingInstance().addGroupId(groupPk); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index fe3812030..6f3683d15 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -285,6 +285,7 @@ async function handleMessageFromPendingMember( groupPk: convoId, author: source, change: { isApproved: true }, + messageHash: null, }); } diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 7518a2f1f..745dd7158 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -619,6 +619,10 @@ export class SwarmPolling { return { namespace, lastHash }; }) ); + window.log.debug( + `namespacesAndLastHashes for ${ed25519Str(pubkey)}:`, + JSON.stringify(namespacesAndLastHashes) + ); const allow401s = type === ConversationTypeEnum.GROUPV2; const results = await SnodeAPIRetrieve.retrieveNextMessagesNoRetries( @@ -630,6 +634,23 @@ export class SwarmPolling { allow401s ); + const namespacesAndLastHashesAfterFetch = await Promise.all( + namespaces.map(async namespace => { + const lastHash = await this.getLastHash(snodeEdkey, pubkey, namespace); + return { namespace, lastHash }; + }) + ); + + if ( + namespacesAndLastHashes.some(m => m) && + namespacesAndLastHashesAfterFetch.every(m => !m) + ) { + window.log.info( + `SwarmPolling: hashes for ${ed25519Str(pubkey)} have been reset while we were fetching new messages. discarding them....` + ); + return []; + } + if (!results.length) { return []; } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 2b5acaa12..bca40faef 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -416,6 +416,7 @@ class ConvoController { } async resetLastHashesForConversation(groupPk: GroupPubkeyType) { + window.log.info(`resetLastHashesForConversation for ${ed25519Str(groupPk)}`); await getSwarmPollingInstance().resetLastHashesForConversation(groupPk); await Data.emptySeenMessageHashesForConversation(groupPk); } diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index bf89c4a5b..c30023a6b 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -34,6 +34,7 @@ import { PreConditionFailed } from '../utils/errors'; import { ConversationTypeEnum } from '../../models/types'; import { NetworkTime } from '../../util/NetworkTime'; import { MessageQueue } from '../sending'; +import type { WithMessageHashOrNull } from '../types/with'; export type GroupInfo = { id: string; @@ -128,6 +129,7 @@ async function initiateClosedGroupUpdate( const dbMessageName = await addUpdateMessage({ diff: nameOnlyDiff, ...sharedDetails, + messageHash: null, // this is legacy groups }); await sendNewName(convo, diff.newName, dbMessageName.id as string); } @@ -138,6 +140,7 @@ async function initiateClosedGroupUpdate( const dbMessageAdded = await addUpdateMessage({ diff: joiningOnlyDiff, ...sharedDetails, + messageHash: null, // this is legacy groups }); await sendAddedMembers(convo, diff.added, dbMessageAdded.id as string, updateObj); } @@ -148,6 +151,7 @@ async function initiateClosedGroupUpdate( const dbMessageLeaving = await addUpdateMessage({ diff: leavingOnlyDiff, ...sharedDetails, + messageHash: null, // this is legacy groups }); await sendRemovedMembers(convo, diff.kicked, updatedMembers, dbMessageLeaving.id as string); } @@ -161,7 +165,8 @@ export async function addUpdateMessage({ sentAt, expireUpdate, markAlreadySent, -}: { + messageHash, +}: WithMessageHashOrNull & { convo: ConversationModel; diff: GroupDiff; sender: string; @@ -197,6 +202,7 @@ export async function addUpdateMessage({ source: sender, conversationId: convo.id, type: isUs ? 'outgoing' : 'incoming', + messageHash: messageHash || undefined, }; /** diff --git a/ts/session/types/with.ts b/ts/session/types/with.ts index edc9f8bd0..e2503eb3a 100644 --- a/ts/session/types/with.ts +++ b/ts/session/types/with.ts @@ -2,6 +2,7 @@ import { PubkeyType } from 'libsession_util_nodejs'; import { Snode } from '../../data/types'; export type WithMessageHash = { messageHash: string }; +export type WithMessageHashOrNull = { messageHash: string | null }; export type WithTimestamp = { timestamp: number }; export type WithSignature = { signature: string }; export type WithSecretKey = { secretKey: Uint8Array }; diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index bdc2dce89..c1bafc0f8 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -205,6 +205,7 @@ const initNewGroupInWrapper = createAsyncThunk( sentAt, convo, markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier + messageHash: null, }); groupMemberChange = await GroupUpdateMessageFactory.getWithoutHistoryControlMessage({ adminSecretKey: groupSecretKey, @@ -624,6 +625,7 @@ async function handleMemberAddedFromUI({ sentAt: createAtNetworkTimestamp, expireUpdate: expireDetails, markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier + messageHash: null, }; const updateMessagesToPush: Array = []; if (withHistory.length) { @@ -779,6 +781,7 @@ async function handleMemberRemovedFromUI({ : null, }, markAlreadySent: false, // the store below will mark the message as sent using dbMsgIdentifier + messageHash: null, }); removedControlMessage = await GroupUpdateMessageFactory.getRemovedControlMessage({ adminSecretKey: group.secretKey, @@ -864,6 +867,7 @@ async function handleNameChangeFromUI({ createAtNetworkTimestamp ), markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier + messageHash: null, }); // we want to send an update only if the change was made locally. @@ -992,6 +996,7 @@ const triggerFakeAvatarUpdate = createAsyncThunk( sentAt: createAtNetworkTimestamp, convo, markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier + messageHash: null, }); await msgModel.commit(); @@ -1169,6 +1174,7 @@ const inviteResponseReceived = createAsyncThunk( ); } await GroupSync.queueNewJobIfNeeded(groupPk); + await LibSessionUtil.saveDumpsToDb(groupPk); } catch (e) { window.log.info('inviteResponseReceived failed with', e.message); // only admins can do the steps above, but we don't want to throw if we are not an admin