fix: make group control message not expire

pull/2940/head
Audric Ackermann 1 year ago
parent e03c3ce1f3
commit 3a26285667

@ -384,6 +384,7 @@ export async function setDisappearingMessagesByConvoId(
providedExpireTimer: 0, providedExpireTimer: 0,
fromSync: false, fromSync: false,
fromCurrentDevice: true, fromCurrentDevice: true,
fromConfigMessage: false,
}); });
} else { } else {
await conversation.updateExpireTimer({ await conversation.updateExpireTimer({
@ -391,6 +392,7 @@ export async function setDisappearingMessagesByConvoId(
providedExpireTimer: seconds, providedExpireTimer: seconds,
fromSync: false, fromSync: false,
fromCurrentDevice: true, fromCurrentDevice: true,
fromConfigMessage: false,
}); });
} }
} }

@ -17,6 +17,7 @@ import {
xor, xor,
} from 'lodash'; } from 'lodash';
import { v4 } from 'uuid';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { getMessageQueue } from '../session'; import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
@ -117,7 +118,6 @@ import {
getSubscriberCountOutsideRedux, getSubscriberCountOutsideRedux,
} from '../state/selectors/sogsRoomInfo'; // decide it it makes sense to move this to a redux slice? } from '../state/selectors/sogsRoomInfo'; // decide it it makes sense to move this to a redux slice?
import { v4 } from 'uuid';
import { DisappearingMessages } from '../session/disappearing_messages'; import { DisappearingMessages } from '../session/disappearing_messages';
import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types'; import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types';
import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob'; import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob';
@ -826,7 +826,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
providedExpireTimer, providedExpireTimer,
providedSource, providedSource,
receivedAt, // is set if it comes from outside receivedAt, // is set if it comes from outside
fromSync, // if the update comes from a config or sync message fromSync, // if the update comes from sync message ONLY
fromConfigMessage, // if the update comes from a libsession config message ONLY
fromCurrentDevice, fromCurrentDevice,
shouldCommitConvo = true, shouldCommitConvo = true,
existingMessage, existingMessage,
@ -837,10 +838,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
receivedAt?: number; // is set if it comes from outside receivedAt?: number; // is set if it comes from outside
fromSync: boolean; fromSync: boolean;
fromCurrentDevice: boolean; fromCurrentDevice: boolean;
fromConfigMessage: boolean;
shouldCommitConvo?: boolean; shouldCommitConvo?: boolean;
existingMessage?: MessageModel; existingMessage?: MessageModel;
}): Promise<boolean> { }): Promise<boolean> {
const isRemoteChange = Boolean((receivedAt || fromSync) && !fromCurrentDevice); const isRemoteChange = Boolean(
(receivedAt || fromSync || fromConfigMessage) && !fromCurrentDevice
);
// we don't add an update message when this comes from a config message, as we already have the SyncedMessage itself with the right timestamp to display
const shouldAddExpireUpdateMessage = !fromConfigMessage;
if (this.isPublic()) { if (this.isPublic()) {
throw new Error("updateExpireTimer() Disappearing messages aren't supported in communities"); throw new Error("updateExpireTimer() Disappearing messages aren't supported in communities");
@ -887,6 +894,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}); });
} }
if (!shouldAddExpireUpdateMessage) {
await Conversation.cleanUpExpireHistoryFromConvo(this.id, this.isPrivate());
if (shouldCommitConvo) {
// tell the UI this conversation was updated
await this.commit();
}
return false;
}
let message = existingMessage || undefined; let message = existingMessage || undefined;
const expirationType = DisappearingMessages.changeToDisappearingMessageType( const expirationType = DisappearingMessages.changeToDisappearingMessageType(
this, this,
@ -922,12 +939,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}); });
} }
} }
// force that message to expire with the old disappear setting when the setting was turned off.
// this is to make the update to 'off' disappear with the previous disappearing message setting // Note: we agreed that a closed group ControlMessage message does not expire.
message.set({ message.set({
expirationType, expirationType: this.isClosedGroup() ? 'unknown' : expirationType,
expireTimer, expireTimer: this.isClosedGroup() ? 0 : expireTimer,
}); });
if (!message.get('id')) { if (!message.get('id')) {
message.set({ id: v4() }); message.set({ id: v4() });
} }
@ -950,7 +968,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
); );
if (!message.getExpirationStartTimestamp()) { if (!message.getExpirationStartTimestamp()) {
const canBeDeleteAfterSend = this.isMe() || this.isGroup(); // Note: we agreed that a closed group ControlMessage message does not expire.
const canBeDeleteAfterSend = this.isMe() || !(this.isGroup() && message.isControlMessage());
if ( if (
(canBeDeleteAfterSend && expirationMode === 'legacy') || (canBeDeleteAfterSend && expirationMode === 'legacy') ||
expirationMode === 'deleteAfterSend' expirationMode === 'deleteAfterSend'
@ -1009,13 +1029,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
if (this.isClosedGroup()) { if (this.isClosedGroup()) {
if (this.isAdmin(UserUtils.getOurPubKeyStrFromCache())) { if (this.isAdmin(UserUtils.getOurPubKeyStrFromCache())) {
// NOTE: we agreed that outgoing ExpirationTimerUpdate **for groups** are not expiring,
// but they still need the content to be right(as this is what we use for the change itself)
const expireUpdateForGroup = { const expireUpdateForGroup = {
...expireUpdate, ...expireUpdate,
groupId: this.get('id'), groupId: this.get('id'),
}; };
// NOTE: we agreed that outgoing ExpirationTimerUpdate **for groups** are not expiring.
expireUpdate.expirationType = 'unknown';
expireUpdate.expireTimer = 0;
const expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdateForGroup); const expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdateForGroup);

@ -198,6 +198,15 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return Boolean(flags & expirationTimerFlag) && !isEmpty(this.getExpirationTimerUpdate()); return Boolean(flags & expirationTimerFlag) && !isEmpty(this.getExpirationTimerUpdate());
} }
public isControlMessage() {
return (
this.isExpirationTimerUpdate() ||
this.isDataExtractionNotification() ||
this.isMessageRequestResponse ||
this.isGroupUpdate()
);
}
public isIncoming() { public isIncoming() {
return this.get('type') === 'incoming'; return this.get('type') === 'incoming';
} }
@ -1232,6 +1241,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
throttledAllMessagesDispatch(); throttledAllMessagesDispatch();
} }
private isGroupUpdate() {
return !isEmpty(this.get('group_update'));
}
/** /**
* Before, group_update attributes could be just the string 'You' and not an array. * Before, group_update attributes could be just the string 'You' and not an array.
* Using this method to get the group update makes sure than the joined, kicked, or left are always an array of string, or undefined * Using this method to get the group update makes sure than the joined, kicked, or left are always an array of string, or undefined

@ -325,6 +325,7 @@ export async function handleNewClosedGroup(
receivedAt: GetNetworkTime.getNowWithNetworkOffset(), receivedAt: GetNetworkTime.getNowWithNetworkOffset(),
fromSync: false, fromSync: false,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: false,
}); });
await removeFromCache(envelope); await removeFromCache(envelope);

@ -248,6 +248,7 @@ async function handleUserProfileUpdate(result: IncomingConfResult): Promise<Inco
fromSync: true, fromSync: true,
shouldCommitConvo: false, shouldCommitConvo: false,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: true,
}); });
changes = success; changes = success;
} }
@ -394,6 +395,7 @@ async function handleContactsUpdate(result: IncomingConfResult): Promise<Incomin
fromSync: true, fromSync: true,
fromCurrentDevice: false, fromCurrentDevice: false,
shouldCommitConvo: false, shouldCommitConvo: false,
fromConfigMessage: true,
}); });
changes = changes || success; changes = changes || success;
} }
@ -627,6 +629,7 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
fromSync: true, fromSync: true,
shouldCommitConvo: false, shouldCommitConvo: false,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: true,
}); });
changes = success; changes = success;
} }

@ -13,17 +13,13 @@ import {
deleteMessagesFromSwarmAndCompletelyLocally, deleteMessagesFromSwarmAndCompletelyLocally,
deleteMessagesFromSwarmAndMarkAsDeletedLocally, deleteMessagesFromSwarmAndMarkAsDeletedLocally,
} from '../interactions/conversations/unsendingInteractions'; } from '../interactions/conversations/unsendingInteractions';
import { import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../models/conversationAttributes';
CONVERSATION_PRIORITIES,
ConversationTypeEnum,
READ_MESSAGE_STATE,
} from '../models/conversationAttributes';
import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
import { concatUInt8Array, getSodiumRenderer } from '../session/crypto'; import { concatUInt8Array, getSodiumRenderer } from '../session/crypto';
import { removeMessagePadding } from '../session/crypto/BufferPadding'; import { removeMessagePadding } from '../session/crypto/BufferPadding';
import { DisappearingMessages } from '../session/disappearing_messages'; import { DisappearingMessages } from '../session/disappearing_messages';
import { DisappearingMessageMode } from '../session/disappearing_messages/types'; import { DisappearingMessageUpdate } from '../session/disappearing_messages/types';
import { ProfileManager } from '../session/profile_manager/ProfileManager'; import { ProfileManager } from '../session/profile_manager/ProfileManager';
import { GroupUtils, UserUtils } from '../session/utils'; import { GroupUtils, UserUtils } from '../session/utils';
import { perfEnd, perfStart } from '../session/utils/Performance'; import { perfEnd, perfStart } from '../session/utils/Performance';
@ -482,17 +478,17 @@ export async function innerHandleSwarmContentMessage({
); );
} }
const expireUpdate = await DisappearingMessages.checkForExpireUpdateInContentMessage(
content,
conversationModelForUIUpdate,
messageExpirationFromRetrieve
);
if (content.dataMessage) { if (content.dataMessage) {
// because typescript is funky with incoming protobufs // because typescript is funky with incoming protobufs
if (isEmpty(content.dataMessage.profileKey)) { if (isEmpty(content.dataMessage.profileKey)) {
content.dataMessage.profileKey = null; content.dataMessage.profileKey = null;
} }
const expireUpdate = await DisappearingMessages.checkForExpireUpdateInContentMessage(
content,
conversationModelForUIUpdate,
messageExpirationFromRetrieve
);
// TODO legacy messages support will be removed in a future release // TODO legacy messages support will be removed in a future release
if (expireUpdate?.isDisappearingMessagesV2Released) { if (expireUpdate?.isDisappearingMessagesV2Released) {
await DisappearingMessages.checkHasOutdatedDisappearingMessageClient( await DisappearingMessages.checkHasOutdatedDisappearingMessageClient(
@ -554,7 +550,7 @@ export async function innerHandleSwarmContentMessage({
await handleDataExtractionNotification( await handleDataExtractionNotification(
envelope, envelope,
content.dataExtractionNotification as SignalService.DataExtractionNotification, content.dataExtractionNotification as SignalService.DataExtractionNotification,
content expireUpdate || null
); );
perfEnd( perfEnd(
`handleDataExtractionNotification-${envelope.id}`, `handleDataExtractionNotification-${envelope.id}`,
@ -835,14 +831,14 @@ async function handleMessageRequestResponse(
} }
/** /**
* A DataExtractionNotification message can only come from a 1 o 1 conversation. * A DataExtractionNotification message can only come from a 1o1 conversation.
* *
* We drop them if the convo is not a 1 o 1 conversation. * We drop them if the convo is not a 1o1 conversation.
*/ */
export async function handleDataExtractionNotification( export async function handleDataExtractionNotification(
envelope: EnvelopePlus, envelope: EnvelopePlus,
dataNotificationMessage: SignalService.DataExtractionNotification, dataNotificationMessage: SignalService.DataExtractionNotification,
content: SignalService.Content expireUpdate: DisappearingMessageUpdate | null
): Promise<void> { ): Promise<void> {
// we currently don't care about the timestamp included in the field itself, just the timestamp of the envelope // we currently don't care about the timestamp included in the field itself, just the timestamp of the envelope
const { type, timestamp: referencedAttachment } = dataNotificationMessage; const { type, timestamp: referencedAttachment } = dataNotificationMessage;
@ -852,9 +848,8 @@ export async function handleDataExtractionNotification(
const convo = getConversationController().get(source); const convo = getConversationController().get(source);
if (!convo || !convo.isPrivate()) { if (!convo || !convo.isPrivate()) {
window?.log?.info( window?.log?.info('Got DataNotification for unknown or non-private convo');
'Got DataNotification for unknown or non private convo or read receipt not enabled'
);
return; return;
} }
@ -866,34 +861,8 @@ export async function handleDataExtractionNotification(
const envelopeTimestamp = toNumber(timestamp); const envelopeTimestamp = toNumber(timestamp);
const referencedAttachmentTimestamp = toNumber(referencedAttachment); const referencedAttachmentTimestamp = toNumber(referencedAttachment);
const expireTimer = content.expirationTimer || 0;
const expirationMode = DisappearingMessages.changeToDisappearingConversationMode(
convo,
DisappearingMessageMode[content.expirationType],
expireTimer
);
let expirationType;
let expirationStartTimestamp;
if (convo && expirationMode && expireTimer > 0) {
expirationType =
expirationMode !== 'off'
? DisappearingMessages.changeToDisappearingMessageType(convo, expireTimer, expirationMode)
: undefined;
// NOTE Triggers disappearing for an incoming DataExtractionNotification message
// TODO legacy messages support will be removed in a future release
if (expirationMode === 'legacy' || expirationMode === 'deleteAfterSend') {
expirationStartTimestamp = DisappearingMessages.setExpirationStartTimestamp(
expirationMode,
undefined,
'handleDataExtractionNotification'
);
}
}
await convo.addSingleIncomingMessage({ let created = await convo.addSingleIncomingMessage({
source, source,
sent_at: envelopeTimestamp, sent_at: envelopeTimestamp,
dataExtractionNotification: { dataExtractionNotification: {
@ -901,12 +870,13 @@ export async function handleDataExtractionNotification(
referencedAttachmentTimestamp, // currently unused referencedAttachmentTimestamp, // currently unused
source, source,
}, },
unread: READ_MESSAGE_STATE.unread,
expirationType,
expireTimer,
expirationStartTimestamp,
}); });
created = DisappearingMessages.getMessageReadyToDisappear(
convo,
created,
0,
expireUpdate || undefined
);
await created.commit();
convo.updateLastMessage(); convo.updateLastMessage();
} }

@ -434,6 +434,7 @@ export async function handleMessageJob(
existingMessage: messageModel, existingMessage: messageModel,
shouldCommitConvo: false, shouldCommitConvo: false,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: false,
// NOTE we don't commit yet because we want to get the message id, see below // NOTE we don't commit yet because we want to get the message id, see below
}); });
} else { } else {

@ -399,11 +399,15 @@ function checkForExpiringOutgoingMessage(message: MessageModel, location?: strin
const expireTimer = message.getExpireTimerSeconds(); const expireTimer = message.getExpireTimerSeconds();
const expirationType = message.getExpirationType(); const expirationType = message.getExpirationType();
const isGroupConvo = !!convo?.isClosedGroup();
const isControlMessage = message.isControlMessage();
if ( if (
convo && convo &&
expirationType && expirationType &&
expireTimer > 0 && expireTimer > 0 &&
Boolean(message.getExpirationStartTimestamp()) === false !message.getExpirationStartTimestamp() &&
!(isGroupConvo && isControlMessage)
) { ) {
const expirationMode = changeToDisappearingConversationMode(convo, expirationType, expireTimer); const expirationMode = changeToDisappearingConversationMode(convo, expirationType, expireTimer);
@ -444,6 +448,24 @@ function getMessageReadyToDisappear(
messageExpirationFromRetrieve, messageExpirationFromRetrieve,
} = expireUpdate; } = expireUpdate;
// This message is an ExpirationTimerUpdate
if (messageFlags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) {
const expirationTimerUpdate = {
expirationType,
expireTimer,
source: messageModel.get('source'),
};
messageModel.set({
expirationTimerUpdate,
});
}
// Note: We agreed that a control message for legacy groups does not expire
if (conversationModel.isClosedGroup() && messageModel.isControlMessage()) {
return messageModel;
}
/** /**
* This is quite tricky, but when we receive a message from the network, it might be a disappearing after read one, which was already read by another device. * This is quite tricky, but when we receive a message from the network, it might be a disappearing after read one, which was already read by another device.
* If that's the case, we need to not only mark the message as read, but also mark it as read at the right time. * If that's the case, we need to not only mark the message as read, but also mark it as read at the right time.
@ -496,19 +518,6 @@ function getMessageReadyToDisappear(
}); });
} }
// This message is an ExpirationTimerUpdate
if (messageFlags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) {
const expirationTimerUpdate = {
expirationType,
expireTimer,
source: messageModel.get('source'),
};
messageModel.set({
expirationTimerUpdate,
});
}
return messageModel; return messageModel;
} }

@ -109,12 +109,8 @@ export async function initiateClosedGroupUpdate(
const sharedDetails = { const sharedDetails = {
sender: UserUtils.getOurPubKeyStrFromCache(), sender: UserUtils.getOurPubKeyStrFromCache(),
sentAt: Date.now(), sentAt: Date.now(),
// Note: we agreed that legacy group control messages do not expire
expireUpdate: { expireUpdate: null,
expirationType: groupDetails.expirationType || ('unknown' as const),
expirationTimer: expireTimer || 0,
messageExpirationFromRetrieve: GetNetworkTime.getNowWithNetworkOffset() + expireTimer * 1000,
},
convo, convo,
}; };

@ -561,6 +561,7 @@ describe('DisappearingMessage', () => {
shouldCommitConvo: false, shouldCommitConvo: false,
existingMessage: undefined, existingMessage: undefined,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: false,
}); });
await expect(promise).is.rejectedWith( await expect(promise).is.rejectedWith(
"updateExpireTimer() Disappearing messages aren't supported in communities" "updateExpireTimer() Disappearing messages aren't supported in communities"
@ -590,6 +591,7 @@ describe('DisappearingMessage', () => {
shouldCommitConvo: false, shouldCommitConvo: false,
existingMessage: undefined, existingMessage: undefined,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: false,
}); });
expect(updateSuccess, 'should be true').to.be.true; expect(updateSuccess, 'should be true').to.be.true;
}); });
@ -613,6 +615,7 @@ describe('DisappearingMessage', () => {
shouldCommitConvo: false, shouldCommitConvo: false,
existingMessage: undefined, existingMessage: undefined,
fromCurrentDevice: false, fromCurrentDevice: false,
fromConfigMessage: false,
}); });
expect(updateSuccess, 'should be true').to.be.true; expect(updateSuccess, 'should be true').to.be.true;
expect( expect(

Loading…
Cancel
Save