signal-desktop/ts/services/storyLoader.ts

180 lines
5.2 KiB
TypeScript
Raw Normal View History

2022-03-04 21:14:52 +00:00
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { pick } from 'lodash';
import type { MessageAttributesType } from '../model-types.d';
import type { StoryDataType } from '../state/ducks/stories';
2022-06-23 20:36:11 +00:00
import * as durations from '../util/durations';
2022-03-04 21:14:52 +00:00
import * as log from '../logging/log';
import dataInterface from '../sql/Client';
2022-11-28 17:19:48 +00:00
import type { GetAllStoriesResultType } from '../sql/Interface';
import {
getAttachmentsForMessage,
getPropsForAttachment,
} from '../state/selectors/message';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
2022-03-04 21:14:52 +00:00
import { isNotNil } from '../util/isNotNil';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
2022-11-16 20:18:02 +00:00
import { DurationInSeconds } from '../util/durations';
2022-11-09 21:11:45 +00:00
import { SIGNAL_ACI } from '../types/SignalConversation';
2022-03-04 21:14:52 +00:00
2022-11-28 17:19:48 +00:00
let storyData: GetAllStoriesResultType | undefined;
2022-03-04 21:14:52 +00:00
export async function loadStories(): Promise<void> {
2022-11-28 17:19:48 +00:00
storyData = await dataInterface.getAllStories({});
2022-05-11 21:02:26 +00:00
await repairUnexpiredStories();
2022-03-04 21:14:52 +00:00
}
export function getStoryDataFromMessageAttributes(
message: MessageAttributesType & {
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
}
2022-03-04 21:14:52 +00:00
): StoryDataType | undefined {
2022-07-01 00:52:03 +00:00
const { attachments, deletedForEveryone } = message;
2022-03-04 21:14:52 +00:00
const unresolvedAttachment = attachments ? attachments[0] : undefined;
2022-07-01 00:52:03 +00:00
if (!unresolvedAttachment && !deletedForEveryone) {
2022-03-04 21:14:52 +00:00
log.warn(
`getStoryDataFromMessageAttributes: ${message.id} does not have an attachment`
);
return;
}
let [attachment] =
2022-07-01 00:52:03 +00:00
unresolvedAttachment && unresolvedAttachment.path
? getAttachmentsForMessage(message)
: [unresolvedAttachment];
2022-03-04 21:14:52 +00:00
2023-01-07 00:55:12 +00:00
// If a story message has a preview property in its attributes then we
// rebuild the textAttachment data structure to contain the all the data it
// needs to fully render the text attachment including the link preview and
// its image.
let preview: LinkPreviewType | undefined;
if (message.preview?.length) {
strictAssert(
message.preview.length === 1,
'getStoryDataFromMessageAttributes: story can have only one preview'
);
[preview] = message.preview;
strictAssert(
attachment?.textAttachment,
'getStoryDataFromMessageAttributes: story must have a ' +
'textAttachment with preview'
);
attachment = {
...attachment,
textAttachment: {
...attachment.textAttachment,
preview: {
...preview,
image: preview.image && getPropsForAttachment(preview.image),
},
},
};
} else if (attachment) {
attachment = getPropsForAttachment(attachment);
}
// for a story, the message should always include the sourceDevice
// but some messages got saved without one in the past (sync-sent)
// we default those to some reasonable values that won't break the app
let sourceDevice: number;
if (message.sourceDevice !== undefined) {
sourceDevice = message.sourceDevice;
} else {
log.error('getStoryDataFromMessageAttributes: undefined sourceDevice');
// storage user.getDevice() should always produce a value after registration
const ourDeviceId = window.storage.user.getDeviceId() ?? -1;
if (message.type === 'outgoing') {
sourceDevice = ourDeviceId;
} else if (message.type === 'incoming') {
sourceDevice = 1;
} else {
sourceDevice = -1;
}
}
2022-03-04 21:14:52 +00:00
return {
attachment,
messageId: message.id,
...pick(message, [
'bodyRanges',
2022-07-01 00:52:03 +00:00
'canReplyToStory',
2022-03-04 21:14:52 +00:00
'conversationId',
2022-04-15 00:08:46 +00:00
'deletedForEveryone',
'hasReplies',
'hasRepliesFromSelf',
2022-04-28 22:06:28 +00:00
'reactions',
2022-09-21 23:54:48 +00:00
'readAt',
2022-03-04 21:14:52 +00:00
'readStatus',
2022-04-15 00:08:46 +00:00
'sendStateByConversationId',
2022-03-04 21:14:52 +00:00
'source',
2023-08-16 20:54:39 +00:00
'sourceServiceId',
2022-07-01 00:52:03 +00:00
'storyDistributionListId',
2022-11-29 02:07:26 +00:00
'storyRecipientsVersion',
2022-03-04 21:14:52 +00:00
'timestamp',
2022-04-15 00:08:46 +00:00
'type',
2022-03-04 21:14:52 +00:00
]),
sourceDevice,
expireTimer: message.expireTimer,
expirationStartTimestamp: dropNull(message.expirationStartTimestamp),
2022-03-04 21:14:52 +00:00
};
}
export function getStoriesForRedux(): Array<StoryDataType> {
strictAssert(storyData, 'storyData has not been loaded');
const stories = storyData
2022-04-28 22:06:28 +00:00
.map(getStoryDataFromMessageAttributes)
2022-03-04 21:14:52 +00:00
.filter(isNotNil);
storyData = undefined;
return stories;
}
2022-05-11 21:02:26 +00:00
async function repairUnexpiredStories(): Promise<void> {
strictAssert(storyData, 'Could not load stories');
2022-11-16 20:18:02 +00:00
const DAY_AS_SECONDS = DurationInSeconds.fromDays(1);
2022-06-23 20:36:11 +00:00
const storiesWithExpiry = storyData
2022-06-23 20:36:11 +00:00
.filter(
story =>
2023-08-16 20:54:39 +00:00
story.sourceServiceId !== SIGNAL_ACI &&
2022-11-09 02:38:19 +00:00
(!story.expirationStartTimestamp ||
!story.expireTimer ||
story.expireTimer > DAY_AS_SECONDS)
2022-06-23 20:36:11 +00:00
)
.map(story => ({
...story,
expirationStartTimestamp: Math.min(story.timestamp, Date.now()),
2022-11-16 20:18:02 +00:00
expireTimer: DurationInSeconds.fromMillis(
Math.min(
Math.floor(story.timestamp + durations.DAY - Date.now()),
durations.DAY
)
2022-06-23 20:36:11 +00:00
),
}));
2022-05-11 21:02:26 +00:00
if (!storiesWithExpiry.length) {
return;
}
log.info(
'repairUnexpiredStories: repairing number of stories',
storiesWithExpiry.length
);
await Promise.all(
storiesWithExpiry.map(messageAttributes => {
return window.Signal.Data.saveMessage(messageAttributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
});
})
);
}