signal-desktop/ts/sql/Client.ts

834 lines
23 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2021-10-07 18:16:51 +00:00
import { ipcRenderer as ipc } from 'electron';
2018-09-21 01:47:19 +00:00
2024-07-22 18:16:33 +00:00
import { groupBy, isTypedArray, last, map, omit } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import { deleteExternalFiles } from '../types/Conversation';
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
2021-09-24 00:49:05 +00:00
import * as Bytes from '../Bytes';
import { createBatcher } from '../util/batcher';
2023-05-04 17:59:02 +00:00
import { assertDev, softAssert } from '../util/assert';
2022-07-28 16:35:29 +00:00
import { mapObjectWithSpec } from '../util/mapObjectWithSpec';
import type { ObjectMappingSpecType } from '../util/mapObjectWithSpec';
import { cleanDataForIpc } from './cleanDataForIpc';
2023-08-16 20:54:39 +00:00
import type { AciString, ServiceIdString } from '../types/ServiceId';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import * as log from '../logging/log';
2024-10-08 21:44:14 +00:00
import { isValidUuid, isValidUuidV7 } from '../util/isValidUuid';
import * as Errors from '../types/errors';
import type { StoredJob } from '../jobs/types';
import { formatJobForInsert } from '../jobs/formatJobForInsert';
import { cleanupMessages } from '../util/cleanup';
2024-08-12 19:54:24 +00:00
import { AccessType, ipcInvoke, doShutdown, removeDB } from './channels';
import type {
2024-07-22 18:16:33 +00:00
ClientInterfaceWrap,
2023-03-04 03:03:15 +00:00
AdjacentMessagesByConversationOptionsType,
2022-07-28 16:35:29 +00:00
AllItemsType,
2024-07-22 18:16:33 +00:00
ServerReadableDirectInterface,
ServerWritableDirectInterface,
ClientReadableInterface,
ClientWritableInterface,
ClientSearchResultMessageType,
ConversationType,
2022-07-28 16:35:29 +00:00
GetConversationRangeCenteredOnMessageResultType,
GetRecentStoryRepliesOptionsType,
IdentityKeyIdType,
IdentityKeyType,
2022-07-28 16:35:29 +00:00
StoredIdentityKeyType,
ItemKeyType,
ItemType,
2022-07-28 16:35:29 +00:00
StoredItemType,
MessageType,
MessageTypeUnhydrated,
PreKeyIdType,
PreKeyType,
2022-07-28 16:35:29 +00:00
StoredPreKeyType,
ServerSearchResultMessageType,
SignedPreKeyIdType,
SignedPreKeyType,
2022-07-28 16:35:29 +00:00
StoredSignedPreKeyType,
KyberPreKeyType,
StoredKyberPreKeyType,
2024-07-22 18:16:33 +00:00
ClientOnlyReadableInterface,
ClientOnlyWritableInterface,
} from './Interface';
import { getMessageIdForLogging } from '../util/idForLogging';
2023-03-20 22:23:53 +00:00
import type { MessageAttributesType } from '../model-types';
2023-04-11 03:54:43 +00:00
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateSnippetAroundMention } from '../util/search';
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const ERASE_STICKERS_KEY = 'erase-stickers';
const ERASE_TEMP_KEY = 'erase-temp';
2019-08-07 00:40:25 +00:00
const ERASE_DRAFTS_KEY = 'erase-drafts';
2024-08-19 20:05:35 +00:00
const ERASE_DOWNLOADS_KEY = 'erase-downloads';
2018-08-08 17:00:33 +00:00
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions';
2024-07-22 18:16:33 +00:00
const PAUSE_WRITE_ACCESS = 'pause-sql-writes';
const RESUME_WRITE_ACCESS = 'resume-sql-writes';
2024-07-22 18:16:33 +00:00
const clientOnlyReadable: ClientOnlyReadableInterface = {
getIdentityKeyById,
getAllIdentityKeys,
getKyberPreKeyById,
getAllKyberPreKeys,
getPreKeyById,
getAllPreKeys,
getSignedPreKeyById,
getAllSignedPreKeys,
getItemById,
getAllItems,
searchMessages,
getRecentStoryReplies,
getOlderMessagesByConversation,
getNewerMessagesByConversation,
2024-07-22 18:16:33 +00:00
getConversationRangeCenteredOnMessage,
};
const clientOnlyWritable: ClientOnlyWritableInterface = {
createOrUpdateIdentityKey,
bulkAddIdentityKeys,
createOrUpdateKyberPreKey,
bulkAddKyberPreKeys,
createOrUpdatePreKey,
bulkAddPreKeys,
createOrUpdateSignedPreKey,
bulkAddSignedPreKeys,
createOrUpdateItem,
updateConversation,
removeConversation,
removeMessage,
removeMessages,
// Client-side only
flushUpdateConversationBatcher,
2024-08-12 19:54:24 +00:00
removeDB,
shutdown,
removeMessagesInConversation,
removeOtherData,
cleanupOrphanedAttachments,
ensureFilePermissions,
};
2024-07-22 18:16:33 +00:00
type ClientOverridesType = ClientOnlyWritableInterface &
2023-05-04 17:59:02 +00:00
Pick<
2024-07-22 18:16:33 +00:00
ClientInterfaceWrap<ServerWritableDirectInterface>,
2023-05-04 17:59:02 +00:00
| 'saveAttachmentDownloadJob'
| 'saveMessage'
| 'saveMessages'
| 'updateConversations'
>;
2024-07-22 18:16:33 +00:00
const clientOnlyWritableOverrides: ClientOverridesType = {
...clientOnlyWritable,
saveAttachmentDownloadJob,
2023-05-04 17:59:02 +00:00
saveMessage,
saveMessages,
updateConversations,
};
2024-07-22 18:16:33 +00:00
type ReadableChannelInterface =
ClientInterfaceWrap<ServerReadableDirectInterface>;
const readableChannel: ReadableChannelInterface = new Proxy(
{} as ReadableChannelInterface,
2023-05-04 17:59:02 +00:00
{
2024-07-22 18:16:33 +00:00
get(_target, name) {
return async (...args: ReadonlyArray<unknown>) =>
ipcInvoke(AccessType.Read, String(name), args);
},
}
);
type WritableChannelInterface =
ClientInterfaceWrap<ServerWritableDirectInterface>;
const writableChannel: WritableChannelInterface = new Proxy(
{} as WritableChannelInterface,
{
get(_target, name) {
return async (...args: ReadonlyArray<unknown>) =>
ipcInvoke(AccessType.Write, String(name), args);
},
}
);
export const DataReader: ClientReadableInterface = new Proxy(
{
...clientOnlyReadable,
} as ClientReadableInterface,
2023-05-04 17:59:02 +00:00
{
get(target, name) {
return async (...args: ReadonlyArray<unknown>) => {
2024-07-22 18:16:33 +00:00
if (Reflect.has(target, name)) {
return Reflect.get(target, name)(...args);
2023-05-04 17:59:02 +00:00
}
2024-07-22 18:16:33 +00:00
return Reflect.get(readableChannel, name)(...args);
2023-05-04 17:59:02 +00:00
};
},
}
);
2024-07-22 18:16:33 +00:00
export const DataWriter: ClientWritableInterface = new Proxy(
{
...clientOnlyWritableOverrides,
} as ClientWritableInterface,
{
get(target, name) {
return async (...args: ReadonlyArray<unknown>) => {
if (Reflect.has(target, name)) {
return Reflect.get(target, name)(...args);
}
return Reflect.get(writableChannel, name)(...args);
};
},
}
);
function _cleanData(
data: unknown
): ReturnType<typeof cleanDataForIpc>['cleaned'] {
const { cleaned, pathsChanged } = cleanDataForIpc(data);
if (pathsChanged.length) {
log.info(
`_cleanData cleaned the following paths: ${pathsChanged.join(', ')}`
);
}
return cleaned;
}
export function _cleanMessageData(
data: ReadonlyDeep<MessageType>
): ReadonlyDeep<MessageType> {
2022-07-28 16:35:29 +00:00
const result = { ...data };
2021-03-04 21:44:57 +00:00
// Ensure that all messages have the received_at set properly
if (!data.received_at) {
assertDev(false, 'received_at was not set on the message');
2023-04-11 03:54:43 +00:00
result.received_at = incrementMessageCounter();
}
if (data.attachments) {
const logId = getMessageIdForLogging(data);
2022-07-28 16:35:29 +00:00
result.attachments = data.attachments.map((attachment, index) => {
if (attachment.data && !isTypedArray(attachment.data)) {
log.warn(
`_cleanMessageData/${logId}: Attachment ${index} had non-array \`data\` field; deleting.`
);
return omit(attachment, ['data']);
}
if (attachment.screenshotData) {
assertDev(
false,
`_cleanMessageData/${logId}: Attachment ${index} had screenshotData field; deleting`
);
return omit(attachment, ['screenshotData']);
}
if (attachment.screenshot?.data) {
assertDev(
false,
`_cleanMessageData/${logId}: Attachment ${index} had screenshot.data field; deleting`
);
return omit(attachment, ['screenshot.data']);
}
if (attachment.thumbnail?.data) {
assertDev(
false,
`_cleanMessageData/${logId}: Attachment ${index} had thumbnail.data field; deleting`
);
return omit(attachment, ['thumbnail.data']);
}
return attachment;
});
}
2022-07-28 16:35:29 +00:00
return _cleanData(omit(result, ['dataMessage']));
}
2022-07-28 16:35:29 +00:00
function specToBytes<Input, Output>(
spec: ObjectMappingSpecType,
data: Input
): Output {
return mapObjectWithSpec<string, Uint8Array>(spec, data, x =>
Bytes.fromBase64(x)
);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
function specFromBytes<Input, Output>(
spec: ObjectMappingSpecType,
data: Input
): Output {
return mapObjectWithSpec<Uint8Array, string>(spec, data, x =>
Bytes.toBase64(x)
);
2018-10-18 01:01:21 +00:00
}
// Top-level calls
2022-07-28 16:35:29 +00:00
async function shutdown(): Promise<void> {
log.info('Client.shutdown');
// Stop accepting new SQL jobs, flush outstanding queue
await doShutdown();
}
2018-10-18 01:01:21 +00:00
// Identity Keys
2022-07-28 16:35:29 +00:00
const IDENTITY_KEY_SPEC = ['publicKey'];
async function createOrUpdateIdentityKey(data: IdentityKeyType): Promise<void> {
const updated: StoredIdentityKeyType = specFromBytes(IDENTITY_KEY_SPEC, data);
2024-07-22 18:16:33 +00:00
await writableChannel.createOrUpdateIdentityKey(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getIdentityKeyById(
id: IdentityKeyIdType
): Promise<IdentityKeyType | undefined> {
2024-07-22 18:16:33 +00:00
const data = await readableChannel.getIdentityKeyById(id);
2022-07-28 16:35:29 +00:00
return specToBytes(IDENTITY_KEY_SPEC, data);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function bulkAddIdentityKeys(
array: Array<IdentityKeyType>
): Promise<void> {
const updated: Array<StoredIdentityKeyType> = map(array, data =>
specFromBytes(IDENTITY_KEY_SPEC, data)
);
2024-07-22 18:16:33 +00:00
await writableChannel.bulkAddIdentityKeys(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllIdentityKeys(): Promise<Array<IdentityKeyType>> {
2024-07-22 18:16:33 +00:00
const keys = await readableChannel.getAllIdentityKeys();
2022-07-28 16:35:29 +00:00
return keys.map(key => specToBytes(IDENTITY_KEY_SPEC, key));
}
2018-10-18 01:01:21 +00:00
// Kyber Pre Keys
const KYBER_PRE_KEY_SPEC = ['data'];
async function createOrUpdateKyberPreKey(data: KyberPreKeyType): Promise<void> {
const updated: StoredKyberPreKeyType = specFromBytes(
KYBER_PRE_KEY_SPEC,
data
);
2024-07-22 18:16:33 +00:00
await writableChannel.createOrUpdateKyberPreKey(updated);
}
async function getKyberPreKeyById(
id: PreKeyIdType
): Promise<KyberPreKeyType | undefined> {
2024-07-22 18:16:33 +00:00
const data = await readableChannel.getPreKeyById(id);
return specToBytes(KYBER_PRE_KEY_SPEC, data);
}
async function bulkAddKyberPreKeys(
array: Array<KyberPreKeyType>
): Promise<void> {
const updated: Array<StoredKyberPreKeyType> = map(array, data =>
specFromBytes(KYBER_PRE_KEY_SPEC, data)
);
2024-07-22 18:16:33 +00:00
await writableChannel.bulkAddKyberPreKeys(updated);
}
async function getAllKyberPreKeys(): Promise<Array<KyberPreKeyType>> {
2024-07-22 18:16:33 +00:00
const keys = await readableChannel.getAllKyberPreKeys();
return keys.map(key => specToBytes(KYBER_PRE_KEY_SPEC, key));
}
2018-10-18 01:01:21 +00:00
// Pre Keys
2022-07-28 16:35:29 +00:00
async function createOrUpdatePreKey(data: PreKeyType): Promise<void> {
const updated: StoredPreKeyType = specFromBytes(PRE_KEY_SPEC, data);
2024-07-22 18:16:33 +00:00
await writableChannel.createOrUpdatePreKey(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getPreKeyById(
id: PreKeyIdType
): Promise<PreKeyType | undefined> {
2024-07-22 18:16:33 +00:00
const data = await readableChannel.getPreKeyById(id);
2022-07-28 16:35:29 +00:00
return specToBytes(PRE_KEY_SPEC, data);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function bulkAddPreKeys(array: Array<PreKeyType>): Promise<void> {
const updated: Array<StoredPreKeyType> = map(array, data =>
specFromBytes(PRE_KEY_SPEC, data)
);
2024-07-22 18:16:33 +00:00
await writableChannel.bulkAddPreKeys(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllPreKeys(): Promise<Array<PreKeyType>> {
2024-07-22 18:16:33 +00:00
const keys = await readableChannel.getAllPreKeys();
2022-07-28 16:35:29 +00:00
return keys.map(key => specToBytes(PRE_KEY_SPEC, key));
}
2018-10-18 01:01:21 +00:00
// Signed Pre Keys
2022-07-28 16:35:29 +00:00
const PRE_KEY_SPEC = ['privateKey', 'publicKey'];
async function createOrUpdateSignedPreKey(
data: SignedPreKeyType
): Promise<void> {
const updated: StoredSignedPreKeyType = specFromBytes(PRE_KEY_SPEC, data);
2024-07-22 18:16:33 +00:00
await writableChannel.createOrUpdateSignedPreKey(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getSignedPreKeyById(
id: SignedPreKeyIdType
): Promise<SignedPreKeyType | undefined> {
2024-07-22 18:16:33 +00:00
const data = await readableChannel.getSignedPreKeyById(id);
2022-07-28 16:35:29 +00:00
return specToBytes(PRE_KEY_SPEC, data);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllSignedPreKeys(): Promise<Array<SignedPreKeyType>> {
2024-07-22 18:16:33 +00:00
const keys = await readableChannel.getAllSignedPreKeys();
2022-07-28 16:35:29 +00:00
return keys.map(key => specToBytes(PRE_KEY_SPEC, key));
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function bulkAddSignedPreKeys(
array: Array<SignedPreKeyType>
): Promise<void> {
const updated: Array<StoredSignedPreKeyType> = map(array, data =>
specFromBytes(PRE_KEY_SPEC, data)
);
2024-07-22 18:16:33 +00:00
await writableChannel.bulkAddSignedPreKeys(updated);
2018-10-18 01:01:21 +00:00
}
// Items
2022-07-28 16:35:29 +00:00
const ITEM_SPECS: Partial<Record<ItemKeyType, ObjectMappingSpecType>> = {
2024-07-15 20:58:55 +00:00
defaultWallpaperPhotoPointer: ['value'],
2022-07-28 16:35:29 +00:00
identityKeyMap: {
key: 'value',
valueSpec: {
isMap: true,
valueSpec: ['privKey', 'pubKey'],
},
},
profileKey: ['value'],
senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'],
subscriberId: ['value'],
2024-07-15 20:58:55 +00:00
backupsSubscriberId: ['value'],
2024-10-18 17:15:03 +00:00
backupEphemeralKey: ['value'],
2024-10-31 17:01:03 +00:00
backupMediaRootKey: ['value'],
manifestRecordIkm: ['value'],
2023-08-18 16:33:41 +00:00
usernameLink: ['value.entropy', 'value.serverId'],
2018-10-18 01:01:21 +00:00
};
2022-07-28 16:35:29 +00:00
async function createOrUpdateItem<K extends ItemKeyType>(
data: ItemType<K>
): Promise<void> {
2018-10-18 01:01:21 +00:00
const { id } = data;
if (!id) {
throw new Error(
'createOrUpdateItem: Provided data did not have a truthy id'
);
}
2022-07-28 16:35:29 +00:00
const spec = ITEM_SPECS[id];
const updated: StoredItemType<K> = spec
? specFromBytes(spec, data)
: (data as unknown as StoredItemType<K>);
2018-10-18 01:01:21 +00:00
2024-07-22 18:16:33 +00:00
await writableChannel.createOrUpdateItem(updated);
2018-10-18 01:01:21 +00:00
}
async function getItemById<K extends ItemKeyType>(
id: K
): Promise<ItemType<K> | undefined> {
2022-07-28 16:35:29 +00:00
const spec = ITEM_SPECS[id];
2024-07-22 18:16:33 +00:00
const data = await readableChannel.getItemById(id);
2018-10-18 01:01:21 +00:00
2023-08-18 16:33:41 +00:00
try {
return spec ? specToBytes(spec, data) : (data as unknown as ItemType<K>);
} catch (error) {
log.warn(`getItemById(${id}): Failed to parse item from spec`, error);
return undefined;
}
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllItems(): Promise<AllItemsType> {
2024-07-22 18:16:33 +00:00
const items = await readableChannel.getAllItems();
const result = Object.create(null);
for (const id of Object.keys(items)) {
const key = id as ItemKeyType;
const value = items[key];
2022-07-28 16:35:29 +00:00
const keys = ITEM_SPECS[key];
try {
const deserializedValue = keys
? (specToBytes(keys, { value }) as ItemType<typeof key>).value
: value;
result[key] = deserializedValue;
} catch (error) {
log.warn(`getAllItems(${id}): Failed to parse item from spec`, error);
}
}
return result;
2018-10-18 01:01:21 +00:00
}
// Conversation
const updateConversationBatcher = createBatcher<ConversationType>({
2021-03-26 00:00:03 +00:00
name: 'sql.Client.updateConversationBatcher',
2019-09-26 19:56:31 +00:00
wait: 500,
maxSize: 20,
processBatch: async (items: Array<ConversationType>) => {
2019-09-26 19:56:31 +00:00
// We only care about the most recent update for each conversation
const byId = groupBy(items, item => item.id);
const ids = Object.keys(byId);
2021-11-11 22:43:05 +00:00
const mostRecent = ids.map((id: string): ConversationType => {
const maybeLast = last(byId[id]);
assertDev(maybeLast !== undefined, 'Empty array in `groupBy` result');
2021-11-11 22:43:05 +00:00
return maybeLast;
});
2018-09-21 01:47:19 +00:00
2019-09-26 19:56:31 +00:00
await updateConversations(mostRecent);
},
});
2024-07-22 18:16:33 +00:00
async function updateConversation(data: ConversationType): Promise<void> {
2019-09-26 19:56:31 +00:00
updateConversationBatcher.add(data);
}
async function flushUpdateConversationBatcher(): Promise<void> {
await updateConversationBatcher.flushAndWait();
}
2019-09-26 19:56:31 +00:00
2022-07-28 16:35:29 +00:00
async function updateConversations(
array: Array<ConversationType>
): Promise<void> {
const { cleaned, pathsChanged } = cleanDataForIpc(array);
assertDev(
!pathsChanged.length,
`Paths were cleaned: ${JSON.stringify(pathsChanged)}`
);
2024-07-22 18:16:33 +00:00
await writableChannel.updateConversations(cleaned);
2018-09-21 01:47:19 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeConversation(id: string): Promise<void> {
2024-07-22 18:16:33 +00:00
const existing = await readableChannel.getConversationById(id);
2018-09-21 01:47:19 +00:00
// Note: It's important to have a fully database-hydrated model to delete here because
// it needs to delete all associated on-disk files along with the database delete.
if (existing) {
2024-07-22 18:16:33 +00:00
await writableChannel.removeConversation(id);
await deleteExternalFiles(existing, {
deleteAttachmentData: window.Signal.Migrations.deleteAttachmentData,
});
2018-09-21 01:47:19 +00:00
}
}
function handleSearchMessageJSON(
messages: Array<ServerSearchResultMessageType>
): Array<ClientSearchResultMessageType> {
return messages.map<ClientSearchResultMessageType>(message => {
const parsedMessage = JSON.parse(message.json);
assertDev(
message.ftsSnippet ?? typeof message.mentionStart === 'number',
'Neither ftsSnippet nor matching mention returned from message search'
);
const snippet =
message.ftsSnippet ??
generateSnippetAroundMention({
body: parsedMessage.body,
mentionStart: message.mentionStart ?? 0,
mentionLength: message.mentionLength ?? 1,
});
return {
json: message.json,
// Empty array is a default value. `message.json` has the real field
bodyRanges: [],
...parsedMessage,
snippet,
};
});
2019-01-14 21:49:58 +00:00
}
async function searchMessages({
query,
options,
2023-08-16 20:54:39 +00:00
contactServiceIdsMatchingQuery,
conversationId,
}: {
query: string;
options?: { limit?: number };
2023-08-16 20:54:39 +00:00
contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
conversationId?: string;
}): Promise<Array<ClientSearchResultMessageType>> {
2024-07-22 18:16:33 +00:00
const messages = await readableChannel.searchMessages({
2019-01-14 21:49:58 +00:00
query,
conversationId,
options,
2023-08-16 20:54:39 +00:00
contactServiceIdsMatchingQuery,
});
return handleSearchMessageJSON(messages);
2018-09-21 01:47:19 +00:00
}
2018-10-18 01:01:21 +00:00
// Message
async function saveMessage(
data: ReadonlyDeep<MessageType>,
2021-12-20 21:04:02 +00:00
options: {
jobToInsert?: Readonly<StoredJob>;
forceSave?: boolean;
ourAci: AciString;
2021-12-20 21:04:02 +00:00
}
2022-07-28 16:35:29 +00:00
): Promise<string> {
2024-07-22 18:16:33 +00:00
const id = await writableChannel.saveMessage(_cleanMessageData(data), {
...options,
jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert),
});
2024-10-08 21:44:14 +00:00
softAssert(
// Older messages still have `UUIDv4` so don't log errors when encountering
// it.
(!options.forceSave && isValidUuid(id)) || isValidUuidV7(id),
'saveMessage: messageId is not a UUID'
);
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
2018-10-18 01:01:21 +00:00
return id;
}
async function saveMessages(
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
2024-06-03 17:02:25 +00:00
): Promise<Array<string>> {
2024-07-22 18:16:33 +00:00
const result = await writableChannel.saveMessages(
arrayOfMessages.map(message => _cleanMessageData(message)),
options
);
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();
2024-06-03 17:02:25 +00:00
return result;
}
async function removeMessage(
id: string,
options: {
singleProtoJobQueue: SingleProtoJobQueue;
fromSync?: boolean;
}
): Promise<void> {
2024-07-22 18:16:33 +00:00
const message = await readableChannel.getMessageById(id);
// Note: It's important to have a fully database-hydrated model to delete here because
// it needs to delete all associated on-disk files along with the database delete.
if (message) {
2024-07-22 18:16:33 +00:00
await writableChannel.removeMessage(id);
await cleanupMessages([message], {
...options,
2024-07-22 18:16:33 +00:00
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
});
}
}
export async function deleteAndCleanup(
messages: Array<MessageAttributesType>,
logId: string,
options: {
fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue;
}
): Promise<void> {
const ids = messages.map(message => message.id);
log.info(`deleteAndCleanup/${logId}: Deleting ${ids.length} messages...`);
2024-07-22 18:16:33 +00:00
await writableChannel.removeMessages(ids);
log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
await cleanupMessages(messages, {
...options,
2024-07-22 18:16:33 +00:00
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
2023-09-07 20:07:07 +00:00
});
log.info(`deleteAndCleanup/${logId}: Complete`);
2023-03-20 22:23:53 +00:00
}
async function removeMessages(
messageIds: ReadonlyArray<string>,
options: {
fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue;
}
2023-03-20 22:23:53 +00:00
): Promise<void> {
2024-07-22 18:16:33 +00:00
const messages = await readableChannel.getMessagesById(messageIds);
await cleanupMessages(messages, {
...options,
2024-07-22 18:16:33 +00:00
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
});
2024-07-22 18:16:33 +00:00
await writableChannel.removeMessages(messageIds);
2023-03-20 22:23:53 +00:00
}
function handleMessageJSON(
messages: Array<MessageTypeUnhydrated>
): Array<MessageType> {
return messages.map(message => JSON.parse(message.json));
2019-08-09 23:12:29 +00:00
}
async function getNewerMessagesByConversation(
2023-03-04 03:03:15 +00:00
options: AdjacentMessagesByConversationOptionsType
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
2024-07-24 00:31:40 +00:00
const messages =
await readableChannel.getNewerMessagesByConversation(options);
return handleMessageJSON(messages);
}
async function getRecentStoryReplies(
storyId: string,
options?: GetRecentStoryRepliesOptionsType
): Promise<Array<MessageType>> {
2024-07-22 18:16:33 +00:00
const messages = await readableChannel.getRecentStoryReplies(
storyId,
options
);
return handleMessageJSON(messages);
}
async function getOlderMessagesByConversation(
2023-03-04 03:03:15 +00:00
options: AdjacentMessagesByConversationOptionsType
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
2024-07-24 00:31:40 +00:00
const messages =
await readableChannel.getOlderMessagesByConversation(options);
return handleMessageJSON(messages);
}
2023-03-04 03:03:15 +00:00
async function getConversationRangeCenteredOnMessage(
options: AdjacentMessagesByConversationOptionsType
): Promise<GetConversationRangeCenteredOnMessageResultType<MessageType>> {
2024-07-24 00:31:40 +00:00
const result =
await readableChannel.getConversationRangeCenteredOnMessage(options);
return {
...result,
older: handleMessageJSON(result.older),
newer: handleMessageJSON(result.newer),
};
}
async function removeMessagesInConversation(
conversationId: string,
{
2021-01-13 00:42:15 +00:00
logId,
receivedAt,
singleProtoJobQueue,
fromSync,
2021-01-13 00:42:15 +00:00
}: {
fromSync?: boolean;
2021-01-13 00:42:15 +00:00
logId: string;
receivedAt?: number;
singleProtoJobQueue: SingleProtoJobQueue;
2021-01-13 00:42:15 +00:00
}
2022-07-28 16:35:29 +00:00
): Promise<void> {
let messages;
do {
2021-01-13 00:42:15 +00:00
const chunkSize = 20;
log.info(
2021-01-13 00:42:15 +00:00
`removeAllMessagesInConversation/${logId}: Fetching chunk of ${chunkSize} messages`
);
// Yes, we really want the await in the loop. We're deleting a chunk at a
// time so we don't use too much memory.
// eslint-disable-next-line no-await-in-loop
2023-03-04 03:03:15 +00:00
messages = await getOlderMessagesByConversation({
conversationId,
2021-01-13 00:42:15 +00:00
limit: chunkSize,
includeStoryReplies: true,
receivedAt,
storyId: undefined,
});
if (!messages.length) {
return;
}
// eslint-disable-next-line no-await-in-loop
await deleteAndCleanup(messages, logId, { fromSync, singleProtoJobQueue });
} while (messages.length > 0);
}
// Attachment downloads
2022-07-28 16:35:29 +00:00
async function saveAttachmentDownloadJob(
job: AttachmentDownloadJobType
): Promise<void> {
2024-07-22 18:16:33 +00:00
await writableChannel.saveAttachmentDownloadJob(_cleanData(job));
}
2018-10-18 01:01:21 +00:00
// Other
2022-07-28 16:35:29 +00:00
async function cleanupOrphanedAttachments(): Promise<void> {
try {
await invokeWithTimeout(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
} catch (error) {
log.warn(
'sql/Client: cleanupOrphanedAttachments failure',
Errors.toLogFormat(error)
);
}
2018-08-08 17:00:33 +00:00
}
2022-07-28 16:35:29 +00:00
async function ensureFilePermissions(): Promise<void> {
await invokeWithTimeout(ENSURE_FILE_PERMISSIONS);
}
// Note: will need to restart the app after calling this, to set up afresh
2022-07-28 16:35:29 +00:00
async function removeOtherData(): Promise<void> {
await Promise.all([
invokeWithTimeout(ERASE_SQL_KEY),
invokeWithTimeout(ERASE_ATTACHMENTS_KEY),
invokeWithTimeout(ERASE_STICKERS_KEY),
invokeWithTimeout(ERASE_TEMP_KEY),
2024-08-19 20:05:35 +00:00
invokeWithTimeout(ERASE_DOWNLOADS_KEY),
invokeWithTimeout(ERASE_DRAFTS_KEY),
]);
}
async function invokeWithTimeout(name: string): Promise<void> {
return createTaskWithTimeout(
() => ipc.invoke(name),
`callChannel call to ${name}`
)();
}
2024-07-22 18:16:33 +00:00
export function pauseWriteAccess(): Promise<void> {
return invokeWithTimeout(PAUSE_WRITE_ACCESS);
}
export function resumeWriteAccess(): Promise<void> {
return invokeWithTimeout(RESUME_WRITE_ACCESS);
}