Disappearing message cleanups

This commit is contained in:
Evan Hahn 2021-06-16 17:20:17 -05:00 committed by GitHub
parent dfa6fb5d61
commit 03a187097f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 149 additions and 268 deletions

View file

@ -1,4 +1,4 @@
// Copyright 2016-2020 Signal Messenger, LLC // Copyright 2016-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global /* global
@ -96,11 +96,6 @@
sent: true, sent: true,
}); });
if (message.isExpiring() && !expirationStartTimestamp) {
// TODO DESKTOP-1509: use setToExpire once this is TS
await message.setToExpire(false, { skipSave: true });
}
window.Signal.Util.queueUpdateMessage(message.attributes); window.Signal.Util.queueUpdateMessage(message.attributes);
// notify frontend listeners // notify frontend listeners

View file

@ -1,4 +1,4 @@
// Copyright 2016-2020 Signal Messenger, LLC // Copyright 2016-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global /* global
@ -17,6 +17,9 @@
const messages = await window.Signal.Data.getExpiredMessages({ const messages = await window.Signal.Data.getExpiredMessages({
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
}); });
window.log.info(
`destroyExpiredMessages: found ${messages.length} messages to expire`
);
const messageIds = []; const messageIds = [];
const inMemoryMessages = []; const inMemoryMessages = [];
@ -59,20 +62,15 @@
let timeout; let timeout;
async function checkExpiringMessages() { async function checkExpiringMessages() {
// Look up the next expiring message and set a timer to destroy it window.log.info('checkExpiringMessages: checking for expiring messages');
const message = await window.Signal.Data.getNextExpiringMessage({
Message: Whisper.Message,
});
if (!message) { const soonestExpiry = await window.Signal.Data.getSoonestMessageExpiry();
if (!soonestExpiry) {
window.log.info('checkExpiringMessages: found no messages to expire');
return; return;
} }
const expiresAt = message.get('expires_at'); let wait = soonestExpiry - Date.now();
Whisper.ExpiringMessagesListener.nextExpiration = expiresAt;
window.log.info('next message expires', new Date(expiresAt).toISOString());
let wait = expiresAt - Date.now();
// In the past // In the past
if (wait < 0) { if (wait < 0) {
@ -84,6 +82,12 @@
wait = 2147483647; wait = 2147483647;
} }
window.log.info(
`checkExpiringMessages: next message expires ${new Date(
soonestExpiry
).toISOString()}; waiting ${wait} ms before clearing`
);
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(destroyExpiredMessages, wait); timeout = setTimeout(destroyExpiredMessages, wait);
} }
@ -93,7 +97,6 @@
); );
Whisper.ExpiringMessagesListener = { Whisper.ExpiringMessagesListener = {
nextExpiration: null,
init(events) { init(events) {
checkExpiringMessages(); checkExpiringMessages();
events.on('timetravel', debouncedCheckExpiringMessages); events.on('timetravel', debouncedCheckExpiringMessages);

View file

@ -1,4 +1,4 @@
// Copyright 2016-2020 Signal Messenger, LLC // Copyright 2016-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global /* global
@ -97,11 +97,6 @@
sent: true, sent: true,
}); });
if (message.isExpiring() && !expirationStartTimestamp) {
// TODO DESKTOP-1509: use setToExpire once this is TS
await message.setToExpire(false, { skipSave: true });
}
window.Signal.Util.queueUpdateMessage(message.attributes); window.Signal.Util.queueUpdateMessage(message.attributes);
// notify frontend listeners // notify frontend listeners

View file

@ -1,4 +1,4 @@
// Copyright 2017-2020 Signal Messenger, LLC // Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global /* global
@ -120,10 +120,6 @@
); );
message.set({ expirationStartTimestamp }); message.set({ expirationStartTimestamp });
const force = true;
// TODO DESKTOP-1509: use setToExpire once this is TS
await message.setToExpire(force, { skipSave: true });
const conversation = message.getConversation(); const conversation = message.getConversation();
if (conversation) { if (conversation) {
conversation.trigger('expiration-change', message); conversation.trigger('expiration-change', message);

View file

@ -24,14 +24,13 @@ import { routineProfileRefresh } from './routineProfileRefresh';
import { isMoreRecentThan, isOlderThan } from './util/timestamp'; import { isMoreRecentThan, isOlderThan } from './util/timestamp';
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
import { ConversationModel } from './models/conversations'; import { ConversationModel } from './models/conversations';
import { getMessageById } from './models/messages'; import { getMessageById, MessageModel } from './models/messages';
import { createBatcher } from './util/batcher'; import { createBatcher } from './util/batcher';
import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup';
import { initializeAllJobQueues } from './jobs/initializeAllJobQueues'; import { initializeAllJobQueues } from './jobs/initializeAllJobQueues';
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue'; import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
import { ourProfileKeyService } from './services/ourProfileKey'; import { ourProfileKeyService } from './services/ourProfileKey';
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey'; import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
import { setToExpire } from './services/MessageUpdater';
import { LatestQueue } from './util/LatestQueue'; import { LatestQueue } from './util/LatestQueue';
import { parseIntOrThrow } from './util/parseIntOrThrow'; import { parseIntOrThrow } from './util/parseIntOrThrow';
import { import {
@ -1642,7 +1641,7 @@ export async function startApp(): Promise<void> {
window.dispatchEvent(new Event('storage_ready')); window.dispatchEvent(new Event('storage_ready'));
window.log.info('Cleanup: starting...'); window.log.info('Cleanup: starting...');
const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpiresAt( const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpirationStartTimestamp(
{ {
MessageCollection: window.Whisper.MessageCollection, MessageCollection: window.Whisper.MessageCollection,
} }
@ -1652,11 +1651,13 @@ export async function startApp(): Promise<void> {
); );
await Promise.all( await Promise.all(
messagesForCleanup.map(async message => { messagesForCleanup.map(async message => {
assert(
!message.get('expirationStartTimestamp'),
'Cleanup should not have messages with an expirationStartTimestamp'
);
const delivered = message.get('delivered'); const delivered = message.get('delivered');
const sentAt = message.get('sent_at'); const sentAt = message.get('sent_at');
const expirationStartTimestamp = message.get(
'expirationStartTimestamp'
);
if (message.hasErrors()) { if (message.hasErrors()) {
return; return;
@ -1666,12 +1667,7 @@ export async function startApp(): Promise<void> {
window.log.info( window.log.info(
`Cleanup: Starting timer for delivered message ${sentAt}` `Cleanup: Starting timer for delivered message ${sentAt}`
); );
message.set( message.set('expirationStartTimestamp', sentAt);
setToExpire({
...message.attributes,
expirationStartTimestamp: expirationStartTimestamp || sentAt,
})
);
return; return;
} }
@ -1685,6 +1681,9 @@ export async function startApp(): Promise<void> {
} }
}) })
); );
if (messagesForCleanup.length) {
window.Whisper.ExpiringMessagesListener.update();
}
window.log.info('Cleanup: complete'); window.log.info('Cleanup: complete');
window.log.info('listening for registration events'); window.log.info('listening for registration events');
@ -2389,7 +2388,9 @@ export async function startApp(): Promise<void> {
messagesToSave.push(message.attributes); messagesToSave.push(message.attributes);
} }
}); });
await window.Signal.Data.saveMessages(messagesToSave, {}); await window.Signal.Data.saveMessages(messagesToSave, {
Message: MessageModel,
});
} }
function onReconnect() { function onReconnect() {
// We disable notifications on first connect, but the same applies to reconnect. In // We disable notifications on first connect, but the same applies to reconnect. In

View file

@ -152,7 +152,6 @@ export type PropsData = {
isViewOnce: boolean; isViewOnce: boolean;
}; };
previews: Array<LinkPreviewType>; previews: Array<LinkPreviewType>;
isExpired?: boolean;
isTapToView?: boolean; isTapToView?: boolean;
isTapToViewExpired?: boolean; isTapToViewExpired?: boolean;
@ -497,7 +496,7 @@ export class Message extends React.Component<Props, State> {
public checkExpired(): void { public checkExpired(): void {
const now = Date.now(); const now = Date.now();
const { isExpired, expirationTimestamp, expirationLength } = this.props; const { expirationTimestamp, expirationLength } = this.props;
if (!expirationTimestamp || !expirationLength) { if (!expirationTimestamp || !expirationLength) {
return; return;
@ -506,7 +505,7 @@ export class Message extends React.Component<Props, State> {
return; return;
} }
if (isExpired || now >= expirationTimestamp) { if (now >= expirationTimestamp) {
this.setState({ this.setState({
expiring: true, expiring: true,
}); });

View file

@ -1717,6 +1717,7 @@ export async function createGroupV2({
}; };
await window.Signal.Data.saveMessages([createdTheGroupMessage], { await window.Signal.Data.saveMessages([createdTheGroupMessage], {
forceSave: true, forceSave: true,
Message: window.Whisper.Message,
}); });
const model = new window.Whisper.Message(createdTheGroupMessage); const model = new window.Whisper.Message(createdTheGroupMessage);
window.MessageController.register(model.id, model); window.MessageController.register(model.id, model);
@ -2831,6 +2832,7 @@ async function updateGroup(
if (changeMessagesToSave.length > 0) { if (changeMessagesToSave.length > 0) {
await window.Signal.Data.saveMessages(changeMessagesToSave, { await window.Signal.Data.saveMessages(changeMessagesToSave, {
forceSave: true, forceSave: true,
Message: window.Whisper.Message,
}); });
changeMessagesToSave.forEach(changeMessage => { changeMessagesToSave.forEach(changeMessage => {
const model = new window.Whisper.Message(changeMessage); const model = new window.Whisper.Message(changeMessage);

1
ts/model-types.d.ts vendored
View file

@ -90,7 +90,6 @@ export type MessageAttributesType = {
errors?: Array<CustomError>; errors?: Array<CustomError>;
expirationStartTimestamp: number | null; expirationStartTimestamp: number | null;
expireTimer: number; expireTimer: number;
expires_at: number;
groupMigration?: GroupMigrationType; groupMigration?: GroupMigrationType;
group_update: { group_update: {
avatarUpdated: boolean; avatarUpdated: boolean;

View file

@ -1234,9 +1234,6 @@ export class ConversationModel extends window.Backbone
} }
addSingleMessage(message: MessageModel): MessageModel { addSingleMessage(message: MessageModel): MessageModel {
// TODO use MessageUpdater.setToExpire
message.setToExpire();
const { messagesAdded } = window.reduxActions.conversations; const { messagesAdded } = window.reduxActions.conversations;
const isNewMessage = true; const isNewMessage = true;
messagesAdded( messagesAdded(

View file

@ -56,7 +56,7 @@ import { AttachmentType, isImage, isVideo } from '../types/Attachment';
import { MIMEType } from '../types/MIME'; import { MIMEType } from '../types/MIME';
import { LinkPreviewType } from '../types/message/LinkPreviews'; import { LinkPreviewType } from '../types/message/LinkPreviews';
import { ourProfileKeyService } from '../services/ourProfileKey'; import { ourProfileKeyService } from '../services/ourProfileKey';
import { markRead, setToExpire } from '../services/MessageUpdater'; import { markRead } from '../services/MessageUpdater';
import { import {
isDirectConversation, isDirectConversation,
isGroupV1, isGroupV1,
@ -256,8 +256,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
isSelected?: boolean; isSelected?: boolean;
hasExpired?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
quotedMessage: any; quotedMessage: any;
@ -281,10 +279,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.OUR_UUID = window.textsecure.storage.user.getUuid(); this.OUR_UUID = window.textsecure.storage.user.getUuid();
this.on('destroy', this.onDestroy); this.on('destroy', this.onDestroy);
this.on('change:expirationStartTimestamp', this.setToExpire);
this.on('change:expireTimer', this.setToExpire);
this.on('unload', this.unload); this.on('unload', this.unload);
this.setToExpire();
this.on('change', this.notifyRedux); this.on('change', this.notifyRedux);
} }
@ -1035,7 +1030,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
attachments: this.getAttachmentsForMessage(), attachments: this.getAttachmentsForMessage(),
previews: this.getPropsForPreview(), previews: this.getPropsForPreview(),
quote: this.getPropsForQuote(), quote: this.getPropsForQuote(),
isExpired: this.hasExpired,
expirationLength, expirationLength,
expirationTimestamp, expirationTimestamp,
reactions, reactions,
@ -2028,10 +2022,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
} }
onExpired(): void {
this.hasExpired = true;
}
isUnidentifiedDelivery( isUnidentifiedDelivery(
contactId: string, contactId: string,
lookup: Record<string, unknown> lookup: Record<string, unknown>
@ -2163,29 +2153,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return this.get('expireTimer') && this.get('expirationStartTimestamp'); return this.get('expireTimer') && this.get('expirationStartTimestamp');
} }
setToExpire(force = false, options = {}): void {
this.set(setToExpire(this.attributes, { ...options, force }));
}
isExpired(): boolean {
return this.msTilExpire() <= 0;
}
msTilExpire(): number {
if (!this.isExpiring()) {
return Infinity;
}
const now = Date.now();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const start = this.get('expirationStartTimestamp')!;
const delta = this.get('expireTimer') * 1000;
let msFromNow = start + delta - now;
if (msFromNow < 0) {
msFromNow = 0;
}
return msFromNow;
}
getIncomingContact(): ConversationModel | undefined | null { getIncomingContact(): ConversationModel | undefined | null {
if (!this.isIncoming()) { if (!this.isIncoming()) {
return null; return null;

View file

@ -42,46 +42,3 @@ export function getExpiresAt(
? messageAttrs.expirationStartTimestamp + expireTimerMs ? messageAttrs.expirationStartTimestamp + expireTimerMs
: undefined; : undefined;
} }
export function setToExpire(
messageAttrs: MessageAttributesType,
{ force = false, skipSave = false } = {}
): MessageAttributesType {
if (!isExpiring(messageAttrs) || (!force && messageAttrs.expires_at)) {
return messageAttrs;
}
const expiresAt = getExpiresAt(messageAttrs);
if (!expiresAt) {
return messageAttrs;
}
const nextMessageAttributes = {
...messageAttrs,
expires_at: expiresAt,
};
window.log.info('Set message expiration', {
start: messageAttrs.expirationStartTimestamp,
expiresAt,
sentAt: messageAttrs.sent_at,
});
if (messageAttrs.id && !skipSave) {
window.Signal.Util.queueUpdateMessage(nextMessageAttributes);
}
return nextMessageAttributes;
}
function isExpiring(
messageAttrs: Pick<
MessageAttributesType,
'expireTimer' | 'expirationStartTimestamp'
>
): boolean {
return Boolean(
messageAttrs.expireTimer && messageAttrs.expirationStartTimestamp
);
}

View file

@ -189,8 +189,8 @@ const dataInterface: ClientInterface = {
getAllMessageIds, getAllMessageIds,
getMessagesBySentAt, getMessagesBySentAt,
getExpiredMessages, getExpiredMessages,
getOutgoingWithoutExpiresAt, getOutgoingWithoutExpirationStartTimestamp,
getNextExpiringMessage, getSoonestMessageExpiry,
getNextTapToViewMessageTimestampToAgeOut, getNextTapToViewMessageTimestampToAgeOut,
getTapToViewMessagesNeedingErase, getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation, getOlderMessagesByConversation,
@ -997,12 +997,13 @@ async function saveMessage(
async function saveMessages( async function saveMessages(
arrayOfMessages: Array<MessageType>, arrayOfMessages: Array<MessageType>,
{ forceSave }: { forceSave?: boolean } = {} { forceSave, Message }: { forceSave?: boolean; Message: typeof MessageModel }
) { ) {
await channels.saveMessages( await channels.saveMessages(
arrayOfMessages.map(message => _cleanMessageData(message)), arrayOfMessages.map(message => _cleanMessageData(message)),
{ forceSave } { forceSave }
); );
Message.updateTimers();
} }
async function removeMessage( async function removeMessage(
@ -1300,28 +1301,18 @@ async function getExpiredMessages({
return new MessageCollection(messages); return new MessageCollection(messages);
} }
async function getOutgoingWithoutExpiresAt({ async function getOutgoingWithoutExpirationStartTimestamp({
MessageCollection, MessageCollection,
}: { }: {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) { }) {
const messages = await channels.getOutgoingWithoutExpiresAt(); const messages = await channels.getOutgoingWithoutExpirationStartTimestamp();
return new MessageCollection(messages); return new MessageCollection(messages);
} }
async function getNextExpiringMessage({ function getSoonestMessageExpiry() {
Message, return channels.getSoonestMessageExpiry();
}: {
Message: typeof MessageModel;
}) {
const message = await channels.getNextExpiringMessage();
if (message) {
return new Message(message);
}
return null;
} }
async function getNextTapToViewMessageTimestampToAgeOut() { async function getNextTapToViewMessageTimestampToAgeOut() {

View file

@ -221,10 +221,6 @@ export type DataInterface = {
getMessageCount: (conversationId?: string) => Promise<number>; getMessageCount: (conversationId?: string) => Promise<number>;
hasUserInitiatedMessages: (conversationId: string) => Promise<boolean>; hasUserInitiatedMessages: (conversationId: string) => Promise<boolean>;
saveMessages: (
arrayOfMessages: Array<MessageType>,
options: { forceSave?: boolean }
) => Promise<void>;
getAllMessageIds: () => Promise<Array<string>>; getAllMessageIds: () => Promise<Array<string>>;
getMessageMetricsForConversation: ( getMessageMetricsForConversation: (
conversationId: string conversationId: string
@ -313,6 +309,7 @@ export type DataInterface = {
getMessageServerGuidsForSpam: ( getMessageServerGuidsForSpam: (
conversationId: string conversationId: string
) => Promise<Array<string>>; ) => Promise<Array<string>>;
getSoonestMessageExpiry: () => Promise<undefined | number>;
getJobsInQueue(queueType: string): Promise<Array<StoredJob>>; getJobsInQueue(queueType: string): Promise<Array<StoredJob>>;
insertJob(job: Readonly<StoredJob>): Promise<void>; insertJob(job: Readonly<StoredJob>): Promise<void>;
@ -370,8 +367,7 @@ export type ServerInterface = DataInterface & {
conversationId: string; conversationId: string;
ourConversationId: string; ourConversationId: string;
}) => Promise<MessageType | undefined>; }) => Promise<MessageType | undefined>;
getNextExpiringMessage: () => Promise<MessageType | undefined>; getOutgoingWithoutExpirationStartTimestamp: () => Promise<Array<MessageType>>;
getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>; getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
getUnreadCountForConversation: (conversationId: string) => Promise<number>; getUnreadCountForConversation: (conversationId: string) => Promise<number>;
getUnreadByConversationAndMarkRead: ( getUnreadByConversationAndMarkRead: (
@ -416,6 +412,10 @@ export type ServerInterface = DataInterface & {
data: MessageType, data: MessageType,
options: { forceSave?: boolean } options: { forceSave?: boolean }
) => Promise<string>; ) => Promise<string>;
saveMessages: (
arrayOfMessages: Array<MessageType>,
options: { forceSave?: boolean }
) => Promise<void>;
updateConversation: (data: ConversationType) => Promise<void>; updateConversation: (data: ConversationType) => Promise<void>;
// For testing only // For testing only
@ -505,10 +505,7 @@ export type ClientInterface = DataInterface & {
ourConversationId: string; ourConversationId: string;
Message: typeof MessageModel; Message: typeof MessageModel;
}) => Promise<MessageModel | undefined>; }) => Promise<MessageModel | undefined>;
getNextExpiringMessage: (options: { getOutgoingWithoutExpirationStartTimestamp: (options: {
Message: typeof MessageModel;
}) => Promise<MessageModel | null>;
getOutgoingWithoutExpiresAt: (options: {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>; }) => Promise<MessageModelCollectionType>;
getTapToViewMessagesNeedingErase: (options: { getTapToViewMessagesNeedingErase: (options: {
@ -557,6 +554,10 @@ export type ClientInterface = DataInterface & {
data: MessageType, data: MessageType,
options: { forceSave?: boolean; Message: typeof MessageModel } options: { forceSave?: boolean; Message: typeof MessageModel }
) => Promise<string>; ) => Promise<string>;
saveMessages: (
arrayOfMessages: Array<MessageType>,
options: { forceSave?: boolean; Message: typeof MessageModel }
) => Promise<void>;
searchMessages: ( searchMessages: (
query: string, query: string,
options?: { limit?: number } options?: { limit?: number }

View file

@ -33,10 +33,8 @@ import { ReactionType } from '../types/Reactions';
import { StoredJob } from '../jobs/types'; import { StoredJob } from '../jobs/types';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { combineNames } from '../util/combineNames'; import { combineNames } from '../util/combineNames';
import { getExpiresAt } from '../services/MessageUpdater';
import { isNormalNumber } from '../util/isNormalNumber'; import { isNormalNumber } from '../util/isNormalNumber';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import * as iterables from '../util/iterables';
import { ConversationColorType, CustomColorType } from '../types/Colors'; import { ConversationColorType, CustomColorType } from '../types/Colors';
import { import {
@ -180,8 +178,8 @@ const dataInterface: ServerInterface = {
getAllMessageIds, getAllMessageIds,
getMessagesBySentAt, getMessagesBySentAt,
getExpiredMessages, getExpiredMessages,
getOutgoingWithoutExpiresAt, getOutgoingWithoutExpirationStartTimestamp,
getNextExpiringMessage, getSoonestMessageExpiry,
getNextTapToViewMessageTimestampToAgeOut, getNextTapToViewMessageTimestampToAgeOut,
getTapToViewMessagesNeedingErase, getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation, getOlderMessagesByConversation,
@ -1859,6 +1857,38 @@ function updateToSchemaVersion32(currentVersion: number, db: Database) {
console.log('updateToSchemaVersion32: success!'); console.log('updateToSchemaVersion32: success!');
} }
function updateToSchemaVersion33(currentVersion: number, db: Database) {
if (currentVersion >= 33) {
return;
}
db.transaction(() => {
db.exec(`
-- These indexes should exist, but we add "IF EXISTS" for safety.
DROP INDEX IF EXISTS messages_expires_at;
DROP INDEX IF EXISTS messages_without_timer;
ALTER TABLE messages
ADD COLUMN
expiresAt INT
GENERATED ALWAYS
AS (expirationStartTimestamp + (expireTimer * 1000));
CREATE INDEX message_expires_at ON messages (
expiresAt
);
CREATE INDEX outgoing_messages_without_expiration_start_timestamp ON messages (
expireTimer, expirationStartTimestamp, type
)
WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL;
`);
db.pragma('user_version = 33');
})();
console.log('updateToSchemaVersion33: success!');
}
const SCHEMA_VERSIONS = [ const SCHEMA_VERSIONS = [
updateToSchemaVersion1, updateToSchemaVersion1,
updateToSchemaVersion2, updateToSchemaVersion2,
@ -1892,6 +1922,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion30, updateToSchemaVersion30,
updateToSchemaVersion31, updateToSchemaVersion31,
updateToSchemaVersion32, updateToSchemaVersion32,
updateToSchemaVersion33,
]; ];
function updateSchema(db: Database): void { function updateSchema(db: Database): void {
@ -2997,7 +3028,6 @@ function saveMessageSync(
const { const {
body, body,
conversationId, conversationId,
expires_at,
hasAttachments, hasAttachments,
hasFileAttachments, hasFileAttachments,
hasVisualMediaAttachments, hasVisualMediaAttachments,
@ -3024,7 +3054,6 @@ function saveMessageSync(
body: body || null, body: body || null,
conversationId, conversationId,
expirationStartTimestamp: expirationStartTimestamp || null, expirationStartTimestamp: expirationStartTimestamp || null,
expires_at: expires_at || null,
expireTimer: expireTimer || null, expireTimer: expireTimer || null,
hasAttachments: hasAttachments ? 1 : 0, hasAttachments: hasAttachments ? 1 : 0,
hasFileAttachments: hasFileAttachments ? 1 : 0, hasFileAttachments: hasFileAttachments ? 1 : 0,
@ -3053,7 +3082,6 @@ function saveMessageSync(
body = $body, body = $body,
conversationId = $conversationId, conversationId = $conversationId,
expirationStartTimestamp = $expirationStartTimestamp, expirationStartTimestamp = $expirationStartTimestamp,
expires_at = $expires_at,
expireTimer = $expireTimer, expireTimer = $expireTimer,
hasAttachments = $hasAttachments, hasAttachments = $hasAttachments,
hasFileAttachments = $hasFileAttachments, hasFileAttachments = $hasFileAttachments,
@ -3091,7 +3119,6 @@ function saveMessageSync(
body, body,
conversationId, conversationId,
expirationStartTimestamp, expirationStartTimestamp,
expires_at,
expireTimer, expireTimer,
hasAttachments, hasAttachments,
hasFileAttachments, hasFileAttachments,
@ -3114,7 +3141,6 @@ function saveMessageSync(
$body, $body,
$conversationId, $conversationId,
$expirationStartTimestamp, $expirationStartTimestamp,
$expires_at,
$expireTimer, $expireTimer,
$hasAttachments, $hasAttachments,
$hasFileAttachments, $hasFileAttachments,
@ -3243,31 +3269,6 @@ async function getMessageBySender({
return rows.map(row => jsonToObject(row.json)); return rows.map(row => jsonToObject(row.json));
} }
function getExpireData(
expireTimer: number,
readAt?: number
): {
expirationStartTimestamp: number;
expiresAt: number;
} {
const expirationStartTimestamp = Math.min(Date.now(), readAt || Date.now());
const expiresAt = getExpiresAt({
expireTimer,
expirationStartTimestamp,
});
// We are guaranteeing an expirationStartTimestamp above so this should
// definitely return a number.
if (!expiresAt || typeof expiresAt !== 'number') {
assert(false, 'Expected expiresAt to be a number');
}
return {
expirationStartTimestamp,
expiresAt,
};
}
async function getUnreadCountForConversation( async function getUnreadCountForConversation(
conversationId: string conversationId: string
): Promise<number> { ): Promise<number> {
@ -3296,10 +3297,33 @@ async function getUnreadByConversationAndMarkRead(
> { > {
const db = getInstance(); const db = getInstance();
return db.transaction(() => { return db.transaction(() => {
const expirationStartTimestamp = Math.min(Date.now(), readAt ?? Infinity);
db.prepare<Query>(
`
UPDATE messages
SET
expirationStartTimestamp = $expirationStartTimestamp,
json = json_patch(json, $jsonPatch)
WHERE
(
expirationStartTimestamp IS NULL OR
expirationStartTimestamp > $expirationStartTimestamp
) AND
expireTimer IS NOT NULL AND
conversationId = $conversationId AND
received_at <= $newestUnreadId;
`
).run({
conversationId,
expirationStartTimestamp,
jsonPatch: JSON.stringify({ expirationStartTimestamp }),
newestUnreadId,
});
const rows = db const rows = db
.prepare<Query>( .prepare<Query>(
` `
SELECT id, expires_at, expireTimer, expirationStartTimestamp, json SELECT id, json
FROM messages WHERE FROM messages WHERE
unread = $unread AND unread = $unread AND
conversationId = $conversationId AND conversationId = $conversationId AND
@ -3313,10 +3337,6 @@ async function getUnreadByConversationAndMarkRead(
newestUnreadId, newestUnreadId,
}); });
if (!rows.length) {
return [];
}
db.prepare<Query>( db.prepare<Query>(
` `
UPDATE messages UPDATE messages
@ -3335,58 +3355,18 @@ async function getUnreadByConversationAndMarkRead(
unread: 1, unread: 1,
}); });
const rowsWithExpireTimers = iterables.filter(rows, row => row.expireTimer);
const rowsNeedingExpirationUpdates = iterables.filter(
rowsWithExpireTimers,
row =>
!row.expirationStartTimestamp ||
!row.expires_at ||
getExpireData(row.expireTimer, readAt).expirationStartTimestamp <
row.expirationStartTimestamp
);
const expirationStartTimestampUpdates: Iterable<{
id: string;
expirationStartTimestamp: number;
expiresAt: number;
}> = iterables.map(rowsNeedingExpirationUpdates, row => ({
id: row.id,
...getExpireData(row.expireTimer, readAt),
}));
const stmt = db.prepare<Query>(
`
UPDATE messages
SET
expirationStartTimestamp = $expirationStartTimestamp,
expires_at = $expiresAt
WHERE
id = $id;
`
);
const updatedExpireDataByRowId = new Map<
string,
{
expirationStartTimestamp: number;
expiresAt: number;
}
>();
for (const update of expirationStartTimestampUpdates) {
stmt.run(update);
updatedExpireDataByRowId.set(update.id, update);
}
return rows.map(row => { return rows.map(row => {
const json = jsonToObject(row.json); const json = jsonToObject(row.json);
const updatedExpireData = updatedExpireDataByRowId.get(row.id);
return { return {
unread: false, unread: false,
...pick(json, ['id', 'sent_at', 'source', 'sourceUuid', 'type']), ...pick(json, [
...(updatedExpireData 'expirationStartTimestamp',
? { 'id',
expirationStartTimestamp: 'sent_at',
updatedExpireData.expirationStartTimestamp, 'source',
expires_at: updatedExpireData.expiresAt, 'sourceUuid',
} 'type',
: {}), ]),
}; };
}); });
})(); })();
@ -3916,30 +3896,29 @@ async function getExpiredMessages(): Promise<Array<MessageType>> {
.prepare<Query>( .prepare<Query>(
` `
SELECT json FROM messages WHERE SELECT json FROM messages WHERE
expires_at IS NOT NULL AND expiresAt IS NOT NULL AND
expires_at <= $expires_at expiresAt <= $now
ORDER BY expires_at ASC; ORDER BY expiresAt ASC;
` `
) )
.all({ .all({ now });
expires_at: now,
});
return rows.map(row => jsonToObject(row.json)); return rows.map(row => jsonToObject(row.json));
} }
async function getOutgoingWithoutExpiresAt(): Promise<Array<MessageType>> { async function getOutgoingWithoutExpirationStartTimestamp(): Promise<
Array<MessageType>
> {
const db = getInstance(); const db = getInstance();
const rows: JSONRows = db const rows: JSONRows = db
.prepare<EmptyQuery>( .prepare<EmptyQuery>(
` `
SELECT json FROM messages SELECT json FROM messages
INDEXED BY messages_without_timer INDEXED BY outgoing_messages_without_expiration_start_timestamp
WHERE WHERE
expireTimer > 0 AND expireTimer > 0 AND
expires_at IS NULL AND expirationStartTimestamp IS NULL AND
type IS 'outgoing' type IS 'outgoing';
ORDER BY expires_at ASC;
` `
) )
.all(); .all();
@ -3947,26 +3926,21 @@ async function getOutgoingWithoutExpiresAt(): Promise<Array<MessageType>> {
return rows.map(row => jsonToObject(row.json)); return rows.map(row => jsonToObject(row.json));
} }
async function getNextExpiringMessage(): Promise<MessageType | undefined> { async function getSoonestMessageExpiry(): Promise<undefined | number> {
const db = getInstance(); const db = getInstance();
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index // Note: we use `pluck` to only get the first column.
const rows: JSONRows = db const result: null | number = db
.prepare<EmptyQuery>( .prepare<EmptyQuery>(
` `
SELECT json FROM messages SELECT MIN(expiresAt)
WHERE expires_at > 0 FROM messages;
ORDER BY expires_at ASC
LIMIT 1;
` `
) )
.all(); .pluck(true)
.get();
if (!rows || rows.length < 1) { return result || undefined;
return undefined;
}
return jsonToObject(rows[0].json);
} }
async function getNextTapToViewMessageTimestampToAgeOut(): Promise< async function getNextTapToViewMessageTimestampToAgeOut(): Promise<

View file

@ -1,4 +1,4 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -56,7 +56,6 @@ export type OutgoingMessage = Readonly<
// Optional // Optional
body?: string; body?: string;
expires_at?: number;
expireTimer?: number; expireTimer?: number;
messageTimer?: number; // deprecated messageTimer?: number; // deprecated
isViewOnce?: number; isViewOnce?: number;

View file

@ -11,7 +11,9 @@ const updateMessageBatcher = createBatcher<MessageAttributesType>({
maxSize: 50, maxSize: 50,
processBatch: async (messageAttrs: Array<MessageAttributesType>) => { processBatch: async (messageAttrs: Array<MessageAttributesType>) => {
window.log.info('updateMessageBatcher', messageAttrs.length); window.log.info('updateMessageBatcher', messageAttrs.length);
await window.Signal.Data.saveMessages(messageAttrs, {}); await window.Signal.Data.saveMessages(messageAttrs, {
Message: window.Whisper.Message,
});
}, },
}); });
@ -37,6 +39,9 @@ export const saveNewMessageBatcher = createWaitBatcher<MessageAttributesType>({
maxSize: 30, maxSize: 30,
processBatch: async (messageAttrs: Array<MessageAttributesType>) => { processBatch: async (messageAttrs: Array<MessageAttributesType>) => {
window.log.info('saveNewMessageBatcher', messageAttrs.length); window.log.info('saveNewMessageBatcher', messageAttrs.length);
await window.Signal.Data.saveMessages(messageAttrs, { forceSave: true }); await window.Signal.Data.saveMessages(messageAttrs, {
forceSave: true,
Message: window.Whisper.Message,
});
}, },
}); });