Skip to content

Commit 1ea32bf

Browse files
committed
fix: read receipt and DaS/DaR logic
1 parent b0dc4fa commit 1ea32bf

File tree

11 files changed

+160
-97
lines changed

11 files changed

+160
-97
lines changed

ts/models/conversation.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,14 @@ export class ConversationModel extends Model<ConversationAttributes> {
203203
const type = this.get('type');
204204
switch (type) {
205205
case ConversationTypeEnum.PRIVATE:
206-
return this.id;
206+
return `priv:${ed25519Str(this.id)}`;
207207
case ConversationTypeEnum.GROUPV2:
208-
return `group(${ed25519Str(this.id)})`;
208+
return `group:${ed25519Str(this.id)}`;
209209
case ConversationTypeEnum.GROUP: {
210210
if (this.isPublic()) {
211-
return this.id;
211+
return `comm:${this.id}`;
212212
}
213-
return `group(${ed25519Str(this.id)})`;
213+
return `group_legacy:${ed25519Str(this.id)}`;
214214
}
215215
default:
216216
assertUnreachable(type, `idForLogging case not handled for type:"${type}"`);
@@ -815,10 +815,7 @@ export class ConversationModel extends Model<ConversationAttributes> {
815815
const networkTimestamp = NetworkTime.now();
816816

817817
window?.log?.info(
818-
'Sending message to conversation',
819-
this.idForLogging(),
820-
'with networkTimestamp: ',
821-
networkTimestamp
818+
`Sending message to conversation ${this.idForLogging()} with networkTimestamp: ${networkTimestamp}`
822819
);
823820

824821
const attachmentsWithVoiceMessage = attachments
@@ -1012,11 +1009,12 @@ export class ConversationModel extends Model<ConversationAttributes> {
10121009
}
10131010
}
10141011

1015-
// Note: we agreed that a **legacy closed** group ControlMessage message does not expire.
1016-
// Group v2 on the other hand, have expiring disappearing control message
1012+
// Private chats and group v2 are the two types of conversation
1013+
// where this function can be called, and both have expiring
1014+
// disappearing control messages.
10171015
message.set({
1018-
expirationType: this.isClosedGroup() && !this.isClosedGroupV2() ? 'unknown' : expirationType,
1019-
expireTimer: this.isClosedGroup() && !this.isClosedGroupV2() ? 0 : expireTimer,
1016+
expirationType,
1017+
expireTimer,
10201018
});
10211019

10221020
if (!message.id) {

ts/models/message.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ContentMessage } from '../session/messages/outgoing';
88
import { ClosedGroupV2VisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
99
import { PubKey } from '../session/types';
1010
import {
11+
MessageUtils,
1112
UserUtils,
1213
attachmentIdAsStrFromUrl,
1314
uploadAttachmentsToFileServer,
@@ -89,7 +90,7 @@ import {
8990
getPromotedGroupUpdateChangeStr,
9091
} from './groupUpdate';
9192
import { NetworkTime } from '../util/NetworkTime';
92-
import { MessageQueue } from '../session/sending';
93+
import { MessageQueue, MessageSender } from '../session/sending';
9394
import { getTimerNotificationStr } from './timerNotifications';
9495
import { ExpirationTimerUpdate } from '../session/disappearing_messages/types';
9596
import { Model } from './models';
@@ -170,7 +171,7 @@ export class MessageModel extends Model<MessageAttributes> {
170171
}
171172

172173
public idForLogging() {
173-
return `${this.get('source')} ${this.get('sent_at')}`;
174+
return `msg(${this.id}, ${this.getConversation()?.idForLogging()}, ${this.get('sent_at')})`;
174175
}
175176

176177
public isExpirationTimerUpdate() {
@@ -1087,9 +1088,14 @@ export class MessageModel extends Model<MessageAttributes> {
10871088
);
10881089

10891090
if (syncMessage) {
1090-
await MessageQueue.use().sendSyncMessage({
1091-
namespace: SnodeNamespaces.Default,
1092-
message: syncMessage,
1091+
await MessageSender.sendSingleMessage({
1092+
isSyncMessage: true,
1093+
message: await MessageUtils.toRawMessage(
1094+
PubKey.cast(UserUtils.getOurPubKeyStrFromCache()),
1095+
syncMessage,
1096+
SnodeNamespaces.Default
1097+
),
1098+
abortSignal: null,
10931099
});
10941100
}
10951101
}

ts/models/messageFactory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function getSharedAttributesForPublicMessage({
8383
messageHash: '', // we do not care of a messageHash for an opengroup message. we have serverId for that
8484
// NOTE Community messages do not support disappearing messages
8585
expirationStartTimestamp: undefined,
86+
expires_at: undefined,
8687
};
8788
}
8889

ts/receiver/queuedJob.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ async function markConvoAsReadIfOutgoingMessage(
379379
const isOutgoingMessage =
380380
message.get('type') === 'outgoing' || message.get('direction') === 'outgoing';
381381
if (isOutgoingMessage) {
382-
const sentAt = message.get('sent_at') || message.get('serverTimestamp');
382+
const sentAt = message.get('serverTimestamp') || message.get('sent_at');
383383
if (sentAt) {
384384
const expirationType = message.getExpirationType();
385385
const expireTimer = message.getExpireTimerSeconds();
@@ -446,16 +446,21 @@ export async function handleMessageJob(
446446
messageModel.getExpirationType(),
447447
messageModel.getExpireTimerSeconds()
448448
);
449-
450-
if (expirationMode === 'deleteAfterSend') {
451-
messageModel.set({
452-
expirationStartTimestamp: DisappearingMessages.setExpirationStartTimestamp(
453-
expirationMode,
454-
messageModel.get('sent_at'),
455-
'handleMessageJob',
456-
messageModel.id
457-
),
458-
});
449+
const expireTimer = messageModel.getExpireTimerSeconds();
450+
451+
if (expirationMode === 'deleteAfterSend' && expireTimer > 0) {
452+
const expirationStartTimestamp = DisappearingMessages.setExpirationStartTimestamp(
453+
expirationMode,
454+
messageModel.get('sent_at'),
455+
'handleMessageJob',
456+
messageModel.id
457+
);
458+
if (expirationStartTimestamp) {
459+
messageModel.set({
460+
expirationStartTimestamp,
461+
expires_at: expirationStartTimestamp + expireTimer * 1000,
462+
});
463+
}
459464
}
460465
}
461466

ts/session/disappearing_messages/index.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ function setExpirationStartTimestamp(
170170
// these are for debugging purposes
171171
callLocation?: string,
172172
messageId?: string
173-
): number | undefined {
173+
) {
174174
let expirationStartTimestamp: number | undefined = NetworkTime.now();
175175

176176
if (callLocation) {
@@ -184,7 +184,7 @@ function setExpirationStartTimestamp(
184184
if (timestamp) {
185185
if (!isValidUnixTimestamp(timestamp)) {
186186
window.log.debug(
187-
`[setExpirationStartTimestamp] We compared 2 timestamps for a disappearing message (${mode}) and the argument timestamp is not a invalid unix timestamp.${
187+
`[setExpirationStartTimestamp] We compared 2 timestamps for a disappearing message (${mode}) and the argument timestamp is an invalid unix timestamp.${
188188
messageId ? `messageId: ${messageId} ` : ''
189189
}`
190190
);
@@ -361,7 +361,15 @@ async function checkForExpireUpdateInContentMessage(
361361
/**
362362
* Checks if an outgoing message is meant to disappear and if so trigger the timer
363363
*/
364-
function checkForExpiringOutgoingMessage(message: MessageModel, location?: string) {
364+
function checkForExpiringOutgoingMessage({
365+
effectivelyStoredAtMs,
366+
location,
367+
message,
368+
}: {
369+
message: MessageModel;
370+
location: string;
371+
effectivelyStoredAtMs: number;
372+
}) {
365373
const convo = message.getConversation();
366374
const expireTimer = message.getExpireTimerSeconds();
367375
const expirationType = message.getExpirationType();
@@ -379,13 +387,17 @@ function checkForExpiringOutgoingMessage(message: MessageModel, location?: strin
379387
const expirationMode = changeToDisappearingConversationMode(convo, expirationType, expireTimer);
380388

381389
if (expirationMode !== 'off') {
382-
message.set({
383-
expirationStartTimestamp: setExpirationStartTimestamp(
384-
expirationMode,
385-
message.get('sent_at'),
386-
location
387-
),
388-
});
390+
const expirationStartTimestamp = setExpirationStartTimestamp(
391+
expirationMode,
392+
effectivelyStoredAtMs,
393+
location
394+
);
395+
if (expirationStartTimestamp) {
396+
message.set({
397+
expirationStartTimestamp,
398+
expires_at: expirationStartTimestamp + expireTimer * 1000,
399+
});
400+
}
389401
}
390402
}
391403
}
@@ -499,14 +511,17 @@ function getMessageReadyToDisappear(
499511
messageExpirationFromRetrieve > 0
500512
) {
501513
// Note: closed groups control message do not disappear
502-
if (!conversationModel.isClosedGroup() && !messageModel.isControlMessage()) {
503-
const expirationStartTimestamp = messageExpirationFromRetrieve - expireTimer * 1000;
504-
const expires_at = messageExpirationFromRetrieve;
505-
messageModel.set({
506-
expirationStartTimestamp,
507-
expires_at,
508-
});
509-
}
514+
515+
const expirationStartTimestamp = messageExpirationFromRetrieve - expireTimer * 1000;
516+
const expires_at = messageExpirationFromRetrieve;
517+
518+
window.log.debug(
519+
`incoming DaS message,\n\tforcing expirationStartTimestamp to ${expirationStartTimestamp} which is ${(Date.now() - expirationStartTimestamp) / 1000}s ago,\n\tand expires_at to ${expires_at} so with ${(messageExpirationFromRetrieve - Date.now()) / 1000}s left`
520+
);
521+
messageModel.set({
522+
expirationStartTimestamp,
523+
expires_at,
524+
});
510525
}
511526

512527
return messageModel;

ts/session/sending/MessageSender.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ async function handleBatchResultWithSubRequests({
673673
// there are some things we need to do when storing messages
674674
// for groups/legacy groups or user (but not for config messages)
675675
if (isStoreUserInitiatedSubRequest(subRequest)) {
676-
const storedAt = batchResult?.[index]?.body?.t;
676+
const storedAtMs = batchResult?.[index]?.body?.t;
677677
const storedHash = batchResult?.[index]?.body?.hash;
678678
const subRequestStatusCode = batchResult?.[index]?.code;
679679
// TODO: the expiration is due to be returned by the storage server on "store" soon, we will then be able to use it instead of doing the storedAt + ttl logic below
@@ -682,7 +682,7 @@ async function handleBatchResultWithSubRequests({
682682
subRequestStatusCode === 200 &&
683683
!isEmpty(storedHash) &&
684684
isString(storedHash) &&
685-
isNumber(storedAt)
685+
isNumber(storedAtMs)
686686
) {
687687
seenHashes.push({
688688
expiresAt: NetworkTime.now() + TTL_DEFAULT.CONTENT_MESSAGE, // non config msg expire at CONTENT_MESSAGE at most
@@ -694,19 +694,18 @@ async function handleBatchResultWithSubRequests({
694694
// For groups, we can just store that hash directly as the group's swarm is hosting all of the group messages
695695
if (subRequest.dbMessageIdentifier) {
696696
// eslint-disable-next-line no-await-in-loop
697-
await MessageSentHandler.handleSwarmMessageSentSuccess(
698-
{
699-
device: subRequest.destination,
700-
isDestinationClosedGroup: MessageSender.destinationIsClosedGroup(destination),
701-
identifier: subRequest.dbMessageIdentifier,
702-
plainTextBuffer:
703-
subRequest instanceof StoreUserMessageSubRequest
704-
? subRequest.plainTextBuffer
705-
: null,
706-
},
707-
storedAt,
708-
storedHash
709-
);
697+
await MessageSentHandler.handleSwarmMessageSentSuccess({
698+
device: subRequest.destination,
699+
isDestinationClosedGroup: MessageSender.destinationIsClosedGroup(destination),
700+
identifier: subRequest.dbMessageIdentifier,
701+
plainTextBuffer:
702+
subRequest instanceof StoreUserMessageSubRequest ? subRequest.plainTextBuffer : null,
703+
// Note: we cannot override this effective timestamp, as this is the one used as an id for
704+
// quotes and read receipts.
705+
sentAtMs: subRequest.createdAtNetworkTimestamp,
706+
storedAtServerMs: storedAtMs,
707+
storedHash,
708+
});
710709
}
711710
}
712711
}

ts/session/sending/MessageSentHandler.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,26 +59,39 @@ async function handlePublicMessageSentFailure(sentMessage: OpenGroupVisibleMessa
5959
await fetchedMessage.getConversation()?.updateLastMessage();
6060
}
6161

62-
async function handleSwarmMessageSentSuccess(
63-
{
64-
device: destination,
65-
identifier,
66-
isDestinationClosedGroup,
67-
plainTextBuffer,
68-
}: Pick<OutgoingRawMessage, 'device' | 'identifier'> & {
69-
/**
70-
* plainTextBuffer is only required when sending a message to a 1o1,
71-
* as we need it to encrypt it again for our linked devices (synced messages)
72-
*/
73-
plainTextBuffer: Uint8Array | null;
74-
/**
75-
* We must not sync a message when it was sent to a closed group
76-
*/
77-
isDestinationClosedGroup: boolean;
78-
},
79-
effectiveTimestamp: number,
80-
storedHash: string | null
81-
) {
62+
async function handleSwarmMessageSentSuccess({
63+
device: destination,
64+
identifier,
65+
isDestinationClosedGroup,
66+
plainTextBuffer,
67+
sentAtMs,
68+
storedAtServerMs,
69+
storedHash,
70+
}: Pick<OutgoingRawMessage, 'device' | 'identifier'> & {
71+
/**
72+
* plainTextBuffer is only required when sending a message to a 1o1,
73+
* as we need it to encrypt it again for our linked devices (synced messages)
74+
*/
75+
plainTextBuffer: Uint8Array | null;
76+
/**
77+
* We must not sync a message when it was sent to a closed group
78+
*/
79+
isDestinationClosedGroup: boolean;
80+
/**
81+
* The timestamp when the message was reported as stored at by the server.
82+
* This is the one we should use to start the disappearing message timer.
83+
*/
84+
storedAtServerMs: number;
85+
/**
86+
* The timestamp when the message was built locally, i.e. how it will be ordered.
87+
* This timestamp is also used for read receipts identification and quotes.
88+
*/
89+
sentAtMs: number;
90+
/**
91+
* The hash of the message as reported by the server.
92+
*/
93+
storedHash: string | null;
94+
}) {
8295
// The wrappedEnvelope will be set only if the message is not one of OpenGroupV2Message type.
8396
let fetchedMessage = await fetchHandleMessageSentData(identifier);
8497
if (!fetchedMessage) {
@@ -102,16 +115,16 @@ async function handleSwarmMessageSentSuccess(
102115

103116
// A message is synced if we triggered a sync message (sentSync)
104117
// and the current message was sent to our device (so a sync message)
105-
const shouldMarkMessageAsSynced =
106-
(isOurDevice && fetchedMessage.get('sentSync')) || isClosedGroupMessage;
118+
const isPrivateSyncMessage = isOurDevice && fetchedMessage.get('sentSync');
119+
const shouldMarkMessageAsSynced = isPrivateSyncMessage || isClosedGroupMessage;
107120

108121
// Handle the sync logic here
109122
if (shouldTriggerSyncMessage && plainTextBuffer) {
110123
try {
111124
const contentDecoded = SignalService.Content.decode(plainTextBuffer);
112125
if (contentDecoded && contentDecoded.dataMessage) {
113126
try {
114-
await fetchedMessage.sendSyncMessage(contentDecoded, effectiveTimestamp);
127+
await fetchedMessage.sendSyncMessage(contentDecoded, sentAtMs);
115128
const tempFetchMessage = await fetchHandleMessageSentData(identifier);
116129
if (!tempFetchMessage) {
117130
window?.log?.warn(
@@ -141,11 +154,17 @@ async function handleSwarmMessageSentSuccess(
141154
fetchedMessage.set({
142155
sent_to: sentTo,
143156
sent: true,
144-
sent_at: effectiveTimestamp,
157+
sent_at: sentAtMs,
145158
errors: undefined,
146159
});
147160

148-
DisappearingMessages.checkForExpiringOutgoingMessage(fetchedMessage, 'handleMessageSentSuccess');
161+
if (!isPrivateSyncMessage) {
162+
DisappearingMessages.checkForExpiringOutgoingMessage({
163+
message: fetchedMessage,
164+
location: 'handleSwarmMessageSentSuccess',
165+
effectivelyStoredAtMs: storedAtServerMs,
166+
});
167+
}
149168

150169
await fetchedMessage.commit();
151170
fetchedMessage.getConversation()?.updateLastMessage();
@@ -182,6 +201,7 @@ async function handleSwarmMessageSentFailure(
182201
if (fetchedMessage.getExpirationType() && fetchedMessage.getExpireTimerSeconds() > 0) {
183202
fetchedMessage.set({
184203
expirationStartTimestamp: undefined,
204+
expires_at: undefined,
185205
});
186206
window.log.warn(
187207
`[handleSwarmMessageSentFailure] Stopping a message from disappearing until we retry the send operation. messageId: ${fetchedMessage.get(

ts/session/utils/Attachments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export async function uploadLinkPreviewToFileServer(
108108
// some links do not have an image associated, and it makes the whole message fail to send
109109
if (!preview?.image) {
110110
if (!preview) {
111-
window.log.warn('tried to upload file to FileServer without image.. skipping');
111+
window.log.debug('tried to upload file to FileServer without image.. skipping');
112112
}
113113
return preview as any;
114114
}

0 commit comments

Comments
 (0)