signal-desktop/ts/sql/Client.ts

1182 lines
29 KiB
TypeScript
Raw Normal View History

2021-01-22 12:17:15 -06:00
// Copyright 2020-2021 Signal Messenger, LLC
2020-10-30 15:34:04 -05:00
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-await-in-loop */
/* eslint-disable camelcase */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
import { ipcRenderer } from 'electron';
2018-09-20 18:47:19 -07:00
2021-03-04 16:44:57 -05:00
import { cloneDeep, get, groupBy, last, map, omit, set } from 'lodash';
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
import { createBatcher } from '../util/batcher';
import { assert } from '../util/assert';
import { cleanDataForIpc } from './cleanDataForIpc';
import {
ConversationModelCollectionType,
MessageModelCollectionType,
} from '../model-types.d';
import {
AttachmentDownloadJobType,
ClientInterface,
ConversationType,
IdentityKeyType,
ItemType,
MessageType,
MessageTypeUnhydrated,
PreKeyType,
SearchResultMessageType,
SessionType,
SignedPreKeyType,
StickerPackStatusType,
StickerPackType,
StickerType,
UnprocessedType,
} from './Interface';
2021-03-04 16:44:57 -05:00
import Server from './Server';
import { MessageModel } from '../models/messages';
import { ConversationModel } from '../models/conversations';
2021-03-04 16:44:57 -05:00
import { waitForPendingQueries } from './Queueing';
// We listen to a lot of events on ipcRenderer, often on the same channel. This prevents
// any warnings that might be sent to the console in that case.
if (ipcRenderer && ipcRenderer.setMaxListeners) {
ipcRenderer.setMaxListeners(0);
} else {
window.log.warn('sql/Client: ipcRenderer is not available!');
}
const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes
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-06 17:40:25 -07:00
const ERASE_DRAFTS_KEY = 'erase-drafts';
2018-08-08 10:00:33 -07:00
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions';
// Because we can't force this module to conform to an interface, we narrow our exports
// to this one default export, which does conform to the interface.
// Note: In Javascript, you need to access the .default property when requiring it
// https://github.com/microsoft/TypeScript/issues/420
const dataInterface: ClientInterface = {
close,
removeDB,
removeIndexedDBFiles,
2018-10-17 18:01:21 -07:00
createOrUpdateIdentityKey,
getIdentityKeyById,
bulkAddIdentityKeys,
removeIdentityKeyById,
removeAllIdentityKeys,
getAllIdentityKeys,
2018-10-17 18:01:21 -07:00
createOrUpdatePreKey,
getPreKeyById,
bulkAddPreKeys,
removePreKeyById,
removeAllPreKeys,
getAllPreKeys,
2018-10-17 18:01:21 -07:00
createOrUpdateSignedPreKey,
getSignedPreKeyById,
getAllSignedPreKeys,
bulkAddSignedPreKeys,
removeSignedPreKeyById,
removeAllSignedPreKeys,
createOrUpdateItem,
getItemById,
getAllItems,
bulkAddItems,
removeItemById,
removeAllItems,
createOrUpdateSession,
2019-09-26 12:56:31 -07:00
createOrUpdateSessions,
2018-10-17 18:01:21 -07:00
getSessionById,
getSessionsById,
2018-10-17 18:01:21 -07:00
bulkAddSessions,
removeSessionById,
removeSessionsByConversation,
2018-10-17 18:01:21 -07:00
removeAllSessions,
getAllSessions,
2018-10-17 18:01:21 -07:00
2018-09-20 18:47:19 -07:00
getConversationCount,
saveConversation,
saveConversations,
getConversationById,
updateConversation,
2019-09-26 12:56:31 -07:00
updateConversations,
2018-09-20 18:47:19 -07:00
removeConversation,
2020-09-08 20:56:23 -04:00
eraseStorageServiceStateFromConversations,
2018-09-20 18:47:19 -07:00
getAllConversations,
getAllConversationIds,
getAllPrivateConversations,
getAllGroupsInvolvingId,
2019-01-14 13:49:58 -08:00
2018-09-20 18:47:19 -07:00
searchConversations,
2019-01-14 13:49:58 -08:00
searchMessages,
searchMessagesInConversation,
2018-09-20 18:47:19 -07:00
getMessageCount,
saveMessage,
saveMessages,
removeMessage,
2021-01-12 16:42:15 -08:00
removeMessages,
getUnreadByConversation,
getMessageBySender,
getMessageById,
getAllMessageIds,
getMessagesBySentAt,
getExpiredMessages,
getOutgoingWithoutExpiresAt,
getNextExpiringMessage,
2019-06-26 12:33:13 -07:00
getNextTapToViewMessageToAgeOut,
getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation,
getNewerMessagesByConversation,
2020-08-06 17:50:54 -07:00
getLastConversationActivity,
getLastConversationPreview,
getMessageMetricsForConversation,
hasGroupCallHistoryMessage,
migrateConversationMessages,
getUnprocessedCount,
getAllUnprocessed,
getUnprocessedById,
saveUnprocessed,
saveUnprocesseds,
updateUnprocessedAttempts,
updateUnprocessedWithData,
2019-09-26 12:56:31 -07:00
updateUnprocessedsWithData,
removeUnprocessed,
removeAllUnprocessed,
getNextAttachmentDownloadJobs,
saveAttachmentDownloadJob,
resetAttachmentDownloadPending,
setAttachmentDownloadJobPending,
removeAttachmentDownloadJob,
removeAllAttachmentDownloadJobs,
getStickerCount,
createOrUpdateStickerPack,
updateStickerPackStatus,
createOrUpdateSticker,
updateStickerLastUsed,
addStickerPackReference,
deleteStickerPackReference,
deleteStickerPack,
getAllStickerPacks,
getAllStickers,
getRecentStickers,
2021-01-27 14:39:45 -08:00
clearAllErrorStickerPackAttempts,
2019-05-24 16:58:27 -07:00
updateEmojiUsage,
getRecentEmojis,
removeAll,
2018-10-17 18:01:21 -07:00
removeAllConfiguration,
getMessagesNeedingUpgrade,
getMessagesWithVisualMediaAttachments,
getMessagesWithFileAttachments,
// Test-only
_getAllMessages,
// Client-side only
shutdown,
removeAllMessagesInConversation,
removeOtherData,
2018-08-08 10:00:33 -07:00
cleanupOrphanedAttachments,
ensureFilePermissions,
// Client-side only, and test-only
_removeConversations,
};
export default dataInterface;
function _cleanData(
data: unknown
): ReturnType<typeof cleanDataForIpc>['cleaned'] {
const { cleaned, pathsChanged } = cleanDataForIpc(data);
if (pathsChanged.length) {
window.log.info(
`_cleanData cleaned the following paths: ${pathsChanged.join(', ')}`
);
}
return cleaned;
}
function _cleanMessageData(data: MessageType): MessageType {
2021-03-04 16:44:57 -05:00
// Ensure that all messages have the received_at set properly
if (!data.received_at) {
assert(false, 'received_at was not set on the message');
data.received_at = window.Signal.Util.incrementMessageCounter();
}
2021-03-04 16:44:57 -05:00
return _cleanData(omit(data, ['dataMessage']));
}
function keysToArrayBuffer(keys: Array<string>, data: any) {
2018-10-17 18:01:21 -07:00
const updated = cloneDeep(data);
const max = keys.length;
for (let i = 0; i < max; i += 1) {
2018-10-17 18:01:21 -07:00
const key = keys[i];
const value = get(data, key);
if (value) {
set(updated, key, base64ToArrayBuffer(value));
}
}
return updated;
}
function keysFromArrayBuffer(keys: Array<string>, data: any) {
2018-10-17 18:01:21 -07:00
const updated = cloneDeep(data);
const max = keys.length;
for (let i = 0; i < max; i += 1) {
2018-10-17 18:01:21 -07:00
const key = keys[i];
const value = get(data, key);
if (value) {
set(updated, key, arrayBufferToBase64(value));
}
}
return updated;
}
// Top-level calls
async function shutdown() {
2021-03-04 16:44:57 -05:00
await waitForPendingQueries();
// Close database
await close();
}
// Note: will need to restart the app after calling this, to set up afresh
async function close() {
2021-03-04 16:44:57 -05:00
await Server.close();
}
// Note: will need to restart the app after calling this, to set up afresh
async function removeDB() {
2021-03-04 16:44:57 -05:00
await Server.removeDB();
}
async function removeIndexedDBFiles() {
2021-03-04 16:44:57 -05:00
await Server.removeIndexedDBFiles();
}
2018-10-17 18:01:21 -07:00
// Identity Keys
const IDENTITY_KEY_KEYS = ['publicKey'];
async function createOrUpdateIdentityKey(data: IdentityKeyType) {
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, {
...data,
id: window.ConversationController.getConversationId(data.id),
});
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateIdentityKey(updated);
2018-10-17 18:01:21 -07:00
}
async function getIdentityKeyById(identifier: string) {
const id = window.ConversationController.getConversationId(identifier);
if (!id) {
throw new Error('getIdentityKeyById: unable to find conversationId');
}
2021-03-04 16:44:57 -05:00
const data = await Server.getIdentityKeyById(id);
2018-10-17 18:01:21 -07:00
return keysToArrayBuffer(IDENTITY_KEY_KEYS, data);
}
async function bulkAddIdentityKeys(array: Array<IdentityKeyType>) {
2018-10-17 18:01:21 -07:00
const updated = map(array, data =>
keysFromArrayBuffer(IDENTITY_KEY_KEYS, data)
);
2021-03-04 16:44:57 -05:00
await Server.bulkAddIdentityKeys(updated);
2018-10-17 18:01:21 -07:00
}
async function removeIdentityKeyById(identifier: string) {
const id = window.ConversationController.getConversationId(identifier);
if (!id) {
throw new Error('removeIdentityKeyById: unable to find conversationId');
}
2021-03-04 16:44:57 -05:00
await Server.removeIdentityKeyById(id);
2018-10-17 18:01:21 -07:00
}
async function removeAllIdentityKeys() {
2021-03-04 16:44:57 -05:00
await Server.removeAllIdentityKeys();
2018-10-17 18:01:21 -07:00
}
async function getAllIdentityKeys() {
2021-03-04 16:44:57 -05:00
const keys = await Server.getAllIdentityKeys();
return keys.map(key => keysToArrayBuffer(IDENTITY_KEY_KEYS, key));
}
2018-10-17 18:01:21 -07:00
// Pre Keys
async function createOrUpdatePreKey(data: PreKeyType) {
2018-10-17 18:01:21 -07:00
const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data);
2021-03-04 16:44:57 -05:00
await Server.createOrUpdatePreKey(updated);
2018-10-17 18:01:21 -07:00
}
async function getPreKeyById(id: number) {
2021-03-04 16:44:57 -05:00
const data = await Server.getPreKeyById(id);
2018-10-17 18:01:21 -07:00
return keysToArrayBuffer(PRE_KEY_KEYS, data);
}
async function bulkAddPreKeys(array: Array<PreKeyType>) {
2018-10-17 18:01:21 -07:00
const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data));
2021-03-04 16:44:57 -05:00
await Server.bulkAddPreKeys(updated);
2018-10-17 18:01:21 -07:00
}
async function removePreKeyById(id: number) {
2021-03-04 16:44:57 -05:00
await Server.removePreKeyById(id);
2018-10-17 18:01:21 -07:00
}
async function removeAllPreKeys() {
2021-03-04 16:44:57 -05:00
await Server.removeAllPreKeys();
2018-10-17 18:01:21 -07:00
}
async function getAllPreKeys() {
2021-03-04 16:44:57 -05:00
const keys = await Server.getAllPreKeys();
return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key));
}
2018-10-17 18:01:21 -07:00
// Signed Pre Keys
const PRE_KEY_KEYS = ['privateKey', 'publicKey'];
async function createOrUpdateSignedPreKey(data: SignedPreKeyType) {
2018-10-17 18:01:21 -07:00
const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data);
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateSignedPreKey(updated);
2018-10-17 18:01:21 -07:00
}
async function getSignedPreKeyById(id: number) {
2021-03-04 16:44:57 -05:00
const data = await Server.getSignedPreKeyById(id);
2018-10-17 18:01:21 -07:00
return keysToArrayBuffer(PRE_KEY_KEYS, data);
}
async function getAllSignedPreKeys() {
2021-03-04 16:44:57 -05:00
const keys = await Server.getAllSignedPreKeys();
return keys.map((key: SignedPreKeyType) =>
keysToArrayBuffer(PRE_KEY_KEYS, key)
);
2018-10-17 18:01:21 -07:00
}
async function bulkAddSignedPreKeys(array: Array<SignedPreKeyType>) {
2018-10-17 18:01:21 -07:00
const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data));
2021-03-04 16:44:57 -05:00
await Server.bulkAddSignedPreKeys(updated);
2018-10-17 18:01:21 -07:00
}
async function removeSignedPreKeyById(id: number) {
2021-03-04 16:44:57 -05:00
await Server.removeSignedPreKeyById(id);
2018-10-17 18:01:21 -07:00
}
async function removeAllSignedPreKeys() {
2021-03-04 16:44:57 -05:00
await Server.removeAllSignedPreKeys();
2018-10-17 18:01:21 -07:00
}
// Items
const ITEM_KEYS: { [key: string]: Array<string> | undefined } = {
2018-10-17 18:01:21 -07:00
identityKey: ['value.pubKey', 'value.privKey'],
senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'],
2018-10-17 18:01:21 -07:00
signaling_key: ['value'],
profileKey: ['value'],
};
async function createOrUpdateItem(data: ItemType) {
2018-10-17 18:01:21 -07:00
const { id } = data;
if (!id) {
throw new Error(
'createOrUpdateItem: Provided data did not have a truthy id'
);
}
const keys = ITEM_KEYS[id];
const updated = Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data;
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateItem(updated);
2018-10-17 18:01:21 -07:00
}
async function getItemById(id: string) {
2018-10-17 18:01:21 -07:00
const keys = ITEM_KEYS[id];
2021-03-04 16:44:57 -05:00
const data = await Server.getItemById(id);
2018-10-17 18:01:21 -07:00
return Array.isArray(keys) ? keysToArrayBuffer(keys, data) : data;
}
async function getAllItems() {
2021-03-04 16:44:57 -05:00
const items = await Server.getAllItems();
2018-10-17 18:01:21 -07:00
return map(items, item => {
const { id } = item;
const keys = ITEM_KEYS[id];
2018-10-17 18:01:21 -07:00
return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item;
});
}
async function bulkAddItems(array: Array<ItemType>) {
2018-10-17 18:01:21 -07:00
const updated = map(array, data => {
const { id } = data;
const keys = ITEM_KEYS[id];
return keys && Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data;
2018-10-17 18:01:21 -07:00
});
2021-03-04 16:44:57 -05:00
await Server.bulkAddItems(updated);
2018-10-17 18:01:21 -07:00
}
async function removeItemById(id: string) {
2021-03-04 16:44:57 -05:00
await Server.removeItemById(id);
2018-10-17 18:01:21 -07:00
}
async function removeAllItems() {
2021-03-04 16:44:57 -05:00
await Server.removeAllItems();
2018-10-17 18:01:21 -07:00
}
// Sessions
async function createOrUpdateSession(data: SessionType) {
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateSession(data);
2018-10-17 18:01:21 -07:00
}
async function createOrUpdateSessions(array: Array<SessionType>) {
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateSessions(array);
2019-09-26 12:56:31 -07:00
}
async function getSessionById(id: string) {
2021-03-04 16:44:57 -05:00
const session = await Server.getSessionById(id);
2018-10-17 18:01:21 -07:00
return session;
}
async function getSessionsById(id: string) {
2021-03-04 16:44:57 -05:00
const sessions = await Server.getSessionsById(id);
2018-10-17 18:01:21 -07:00
return sessions;
}
async function bulkAddSessions(array: Array<SessionType>) {
2021-03-04 16:44:57 -05:00
await Server.bulkAddSessions(array);
2018-10-17 18:01:21 -07:00
}
async function removeSessionById(id: string) {
2021-03-04 16:44:57 -05:00
await Server.removeSessionById(id);
2018-10-17 18:01:21 -07:00
}
async function removeSessionsByConversation(conversationId: string) {
2021-03-04 16:44:57 -05:00
await Server.removeSessionsByConversation(conversationId);
2018-10-17 18:01:21 -07:00
}
async function removeAllSessions() {
2021-03-04 16:44:57 -05:00
await Server.removeAllSessions();
2018-10-17 18:01:21 -07:00
}
async function getAllSessions() {
2021-03-04 16:44:57 -05:00
const sessions = await Server.getAllSessions();
return sessions;
}
2018-10-17 18:01:21 -07:00
// Conversation
2018-09-20 18:47:19 -07:00
async function getConversationCount() {
2021-03-04 16:44:57 -05:00
return Server.getConversationCount();
2018-09-20 18:47:19 -07:00
}
async function saveConversation(data: ConversationType) {
2021-03-04 16:44:57 -05:00
await Server.saveConversation(data);
2018-09-20 18:47:19 -07:00
}
async function saveConversations(array: Array<ConversationType>) {
2021-03-04 16:44:57 -05:00
await Server.saveConversations(array);
2018-09-20 18:47:19 -07:00
}
async function getConversationById(
id: string,
{ Conversation }: { Conversation: typeof ConversationModel }
) {
2021-03-04 16:44:57 -05:00
const data = await Server.getConversationById(id);
2018-09-20 18:47:19 -07:00
return new Conversation(data);
}
const updateConversationBatcher = createBatcher<ConversationType>({
2019-09-26 12:56:31 -07:00
wait: 500,
maxSize: 20,
processBatch: async (items: Array<ConversationType>) => {
2019-09-26 12:56:31 -07:00
// We only care about the most recent update for each conversation
const byId = groupBy(items, item => item.id);
const ids = Object.keys(byId);
const mostRecent = ids.map(id => last(byId[id]));
2018-09-20 18:47:19 -07:00
2019-09-26 12:56:31 -07:00
await updateConversations(mostRecent);
},
});
function updateConversation(data: ConversationType) {
2019-09-26 12:56:31 -07:00
updateConversationBatcher.add(data);
}
async function updateConversations(array: Array<ConversationType>) {
const { cleaned, pathsChanged } = cleanDataForIpc(array);
assert(
!pathsChanged.length,
`Paths were cleaned: ${JSON.stringify(pathsChanged)}`
);
2021-03-04 16:44:57 -05:00
await Server.updateConversations(cleaned);
2018-09-20 18:47:19 -07:00
}
async function removeConversation(
id: string,
{ Conversation }: { Conversation: typeof ConversationModel }
) {
2018-09-20 18:47:19 -07:00
const existing = await getConversationById(id, { Conversation });
// 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) {
2021-03-04 16:44:57 -05:00
await Server.removeConversation(id);
2018-09-20 18:47:19 -07:00
await existing.cleanup();
}
}
// Note: this method will not clean up external files, just delete from SQL
async function _removeConversations(ids: Array<string>) {
2021-03-04 16:44:57 -05:00
await Server.removeConversation(ids);
2018-09-20 18:47:19 -07:00
}
2020-09-08 20:56:23 -04:00
async function eraseStorageServiceStateFromConversations() {
2021-03-04 16:44:57 -05:00
await Server.eraseStorageServiceStateFromConversations();
}
async function getAllConversations({
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}): Promise<ConversationModelCollectionType> {
2021-03-04 16:44:57 -05:00
const conversations = await Server.getAllConversations();
2018-09-20 18:47:19 -07:00
const collection = new ConversationCollection();
collection.add(conversations);
2018-09-20 18:47:19 -07:00
return collection;
}
async function getAllConversationIds() {
2021-03-04 16:44:57 -05:00
const ids = await Server.getAllConversationIds();
2018-09-20 18:47:19 -07:00
return ids;
}
async function getAllPrivateConversations({
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}) {
2021-03-04 16:44:57 -05:00
const conversations = await Server.getAllPrivateConversations();
2018-09-20 18:47:19 -07:00
const collection = new ConversationCollection();
collection.add(conversations);
2018-09-20 18:47:19 -07:00
return collection;
}
async function getAllGroupsInvolvingId(
id: string,
{
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}
) {
2021-03-04 16:44:57 -05:00
const conversations = await Server.getAllGroupsInvolvingId(id);
2018-09-20 18:47:19 -07:00
const collection = new ConversationCollection();
collection.add(conversations);
2018-09-20 18:47:19 -07:00
return collection;
}
async function searchConversations(query: string) {
2021-03-04 16:44:57 -05:00
const conversations = await Server.searchConversations(query);
2019-01-14 13:49:58 -08:00
return conversations;
}
2018-09-20 18:47:19 -07:00
function handleSearchMessageJSON(messages: Array<SearchResultMessageType>) {
2019-08-09 16:12:29 -07:00
return messages.map(message => ({
...JSON.parse(message.json),
snippet: message.snippet,
}));
}
async function searchMessages(
query: string,
{ limit }: { limit?: number } = {}
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.searchMessages(query, { limit });
2019-08-09 16:12:29 -07:00
return handleSearchMessageJSON(messages);
2019-01-14 13:49:58 -08:00
}
async function searchMessagesInConversation(
query: string,
conversationId: string,
{ limit }: { limit?: number } = {}
2019-01-14 13:49:58 -08:00
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.searchMessagesInConversation(
2019-01-14 13:49:58 -08:00
query,
conversationId,
{ limit }
);
2019-08-09 16:12:29 -07:00
return handleSearchMessageJSON(messages);
2018-09-20 18:47:19 -07:00
}
2018-10-17 18:01:21 -07:00
// Message
2020-05-27 17:37:06 -04:00
async function getMessageCount(conversationId?: string) {
2021-03-04 16:44:57 -05:00
return Server.getMessageCount(conversationId);
}
async function saveMessage(
data: MessageType,
{ forceSave, Message }: { forceSave?: boolean; Message: typeof MessageModel }
) {
2021-03-04 16:44:57 -05:00
const id = await Server.saveMessage(_cleanMessageData(data), {
forceSave,
});
2019-06-26 12:33:13 -07:00
Message.updateTimers();
2018-10-17 18:01:21 -07:00
return id;
}
async function saveMessages(
arrayOfMessages: Array<MessageType>,
{ forceSave }: { forceSave?: boolean } = {}
) {
2021-03-04 16:44:57 -05:00
await Server.saveMessages(
arrayOfMessages.map(message => _cleanMessageData(message)),
{ forceSave }
);
}
async function removeMessage(
id: string,
{ Message }: { Message: typeof MessageModel }
) {
const message = await getMessageById(id, { Message });
// 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) {
2021-03-04 16:44:57 -05:00
await Server.removeMessage(id);
await message.cleanup();
}
}
// Note: this method will not clean up external files, just delete from SQL
2021-01-12 16:42:15 -08:00
async function removeMessages(ids: Array<string>) {
2021-03-04 16:44:57 -05:00
await Server.removeMessages(ids);
}
async function getMessageById(
id: string,
{ Message }: { Message: typeof MessageModel }
) {
2021-03-04 16:44:57 -05:00
const message = await Server.getMessageById(id);
if (!message) {
return null;
}
return new Message(message);
2018-09-20 18:47:19 -07:00
}
// For testing only
async function _getAllMessages({
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
2021-03-04 16:44:57 -05:00
const messages = await Server._getAllMessages();
2018-09-20 18:47:19 -07:00
return new MessageCollection(messages);
}
async function getAllMessageIds() {
2021-03-04 16:44:57 -05:00
const ids = await Server.getAllMessageIds();
return ids;
}
async function getMessageBySender(
{
source,
sourceUuid,
sourceDevice,
sent_at,
}: {
source: string;
sourceUuid: string;
sourceDevice: string;
sent_at: number;
},
{ Message }: { Message: typeof MessageModel }
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getMessageBySender({
source,
sourceUuid,
sourceDevice,
sent_at,
});
if (!messages || !messages.length) {
return null;
}
return new Message(messages[0]);
}
async function getUnreadByConversation(
conversationId: string,
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getUnreadByConversation(conversationId);
return new MessageCollection(messages);
}
function handleMessageJSON(messages: Array<MessageTypeUnhydrated>) {
2019-08-09 16:12:29 -07:00
return messages.map(message => JSON.parse(message.json));
}
async function getOlderMessagesByConversation(
conversationId: string,
{
limit = 100,
receivedAt = Number.MAX_VALUE,
sentAt = Number.MAX_VALUE,
messageId,
MessageCollection,
}: {
limit?: number;
receivedAt?: number;
sentAt?: number;
messageId?: string;
MessageCollection: typeof MessageModelCollectionType;
}
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getOlderMessagesByConversation(conversationId, {
limit,
receivedAt,
sentAt,
messageId,
});
2019-08-09 16:12:29 -07:00
return new MessageCollection(handleMessageJSON(messages));
}
async function getNewerMessagesByConversation(
conversationId: string,
{
limit = 100,
receivedAt = 0,
sentAt = 0,
MessageCollection,
}: {
limit?: number;
receivedAt?: number;
sentAt?: number;
MessageCollection: typeof MessageModelCollectionType;
}
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getNewerMessagesByConversation(conversationId, {
limit,
receivedAt,
sentAt,
});
2019-08-09 16:12:29 -07:00
return new MessageCollection(handleMessageJSON(messages));
}
async function getLastConversationActivity({
conversationId,
ourConversationId,
Message,
}: {
conversationId: string;
ourConversationId: string;
Message: typeof MessageModel;
}): Promise<MessageModel | undefined> {
2021-03-04 16:44:57 -05:00
const result = await Server.getLastConversationActivity({
conversationId,
ourConversationId,
});
2020-08-06 17:50:54 -07:00
if (result) {
return new Message(result);
}
return undefined;
2020-08-06 17:50:54 -07:00
}
async function getLastConversationPreview({
conversationId,
ourConversationId,
Message,
}: {
conversationId: string;
ourConversationId: string;
Message: typeof MessageModel;
}): Promise<MessageModel | undefined> {
2021-03-04 16:44:57 -05:00
const result = await Server.getLastConversationPreview({
conversationId,
ourConversationId,
});
2020-08-06 17:50:54 -07:00
if (result) {
return new Message(result);
}
return undefined;
2020-08-06 17:50:54 -07:00
}
async function getMessageMetricsForConversation(conversationId: string) {
2021-03-04 16:44:57 -05:00
const result = await Server.getMessageMetricsForConversation(conversationId);
return result;
}
function hasGroupCallHistoryMessage(
conversationId: string,
eraId: string
): Promise<boolean> {
2021-03-04 16:44:57 -05:00
return Server.hasGroupCallHistoryMessage(conversationId, eraId);
}
async function migrateConversationMessages(
obsoleteId: string,
currentId: string
) {
2021-03-04 16:44:57 -05:00
await Server.migrateConversationMessages(obsoleteId, currentId);
}
async function removeAllMessagesInConversation(
conversationId: string,
{
2021-01-12 16:42:15 -08:00
logId,
MessageCollection,
2021-01-12 16:42:15 -08:00
}: {
logId: string;
MessageCollection: typeof MessageModelCollectionType;
}
) {
let messages;
do {
2021-01-12 16:42:15 -08:00
const chunkSize = 20;
window.log.info(
`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.
messages = await getOlderMessagesByConversation(conversationId, {
2021-01-12 16:42:15 -08:00
limit: chunkSize,
MessageCollection,
});
if (!messages.length) {
return;
}
const ids = messages.map((message: MessageModel) => message.id);
2021-01-12 16:42:15 -08:00
window.log.info(`removeAllMessagesInConversation/${logId}: Cleanup...`);
// Note: It's very important that these models are fully hydrated because
// we need to delete all associated on-disk files along with the database delete.
2021-01-12 16:42:15 -08:00
const queue = new window.PQueue({ concurrency: 3, timeout: 1000 * 60 * 2 });
queue.addAll(
messages.map((message: MessageModel) => async () => message.cleanup())
);
2021-01-12 16:42:15 -08:00
await queue.onIdle();
2021-01-12 16:42:15 -08:00
window.log.info(`removeAllMessagesInConversation/${logId}: Deleting...`);
2021-03-04 16:44:57 -05:00
await Server.removeMessages(ids);
} while (messages.length > 0);
}
async function getMessagesBySentAt(
sentAt: number,
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getMessagesBySentAt(sentAt);
return new MessageCollection(messages);
}
async function getExpiredMessages({
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getExpiredMessages();
return new MessageCollection(messages);
}
async function getOutgoingWithoutExpiresAt({
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getOutgoingWithoutExpiresAt();
return new MessageCollection(messages);
}
async function getNextExpiringMessage({
Message,
}: {
Message: typeof MessageModel;
}) {
2021-03-04 16:44:57 -05:00
const message = await Server.getNextExpiringMessage();
if (message) {
return new Message(message);
}
return null;
}
async function getNextTapToViewMessageToAgeOut({
Message,
}: {
Message: typeof MessageModel;
}) {
2021-03-04 16:44:57 -05:00
const message = await Server.getNextTapToViewMessageToAgeOut();
2019-06-26 12:33:13 -07:00
if (!message) {
return null;
}
return new Message(message);
}
async function getTapToViewMessagesNeedingErase({
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getTapToViewMessagesNeedingErase();
2019-06-26 12:33:13 -07:00
return new MessageCollection(messages);
}
2018-10-17 18:01:21 -07:00
// Unprocessed
async function getUnprocessedCount() {
2021-03-04 16:44:57 -05:00
return Server.getUnprocessedCount();
}
async function getAllUnprocessed() {
2021-03-04 16:44:57 -05:00
return Server.getAllUnprocessed();
}
async function getUnprocessedById(id: string) {
2021-03-04 16:44:57 -05:00
return Server.getUnprocessedById(id);
}
async function saveUnprocessed(
data: UnprocessedType,
{ forceSave }: { forceSave?: boolean } = {}
) {
2021-03-04 16:44:57 -05:00
const id = await Server.saveUnprocessed(_cleanData(data), { forceSave });
return id;
}
async function saveUnprocesseds(
arrayOfUnprocessed: Array<UnprocessedType>,
{ forceSave }: { forceSave?: boolean } = {}
) {
2021-03-04 16:44:57 -05:00
await Server.saveUnprocesseds(_cleanData(arrayOfUnprocessed), {
forceSave,
});
}
async function updateUnprocessedAttempts(id: string, attempts: number) {
2021-03-04 16:44:57 -05:00
await Server.updateUnprocessedAttempts(id, attempts);
}
async function updateUnprocessedWithData(id: string, data: UnprocessedType) {
2021-03-04 16:44:57 -05:00
await Server.updateUnprocessedWithData(id, data);
}
async function updateUnprocessedsWithData(array: Array<UnprocessedType>) {
2021-03-04 16:44:57 -05:00
await Server.updateUnprocessedsWithData(array);
2019-09-26 12:56:31 -07:00
}
2021-02-26 15:42:45 -08:00
async function removeUnprocessed(id: string | Array<string>) {
2021-03-04 16:44:57 -05:00
await Server.removeUnprocessed(id);
}
async function removeAllUnprocessed() {
2021-03-04 16:44:57 -05:00
await Server.removeAllUnprocessed();
}
// Attachment downloads
async function getNextAttachmentDownloadJobs(
limit?: number,
options?: { timestamp?: number }
) {
2021-03-04 16:44:57 -05:00
return Server.getNextAttachmentDownloadJobs(limit, options);
}
async function saveAttachmentDownloadJob(job: AttachmentDownloadJobType) {
2021-03-04 16:44:57 -05:00
await Server.saveAttachmentDownloadJob(_cleanData(job));
}
async function setAttachmentDownloadJobPending(id: string, pending: boolean) {
2021-03-04 16:44:57 -05:00
await Server.setAttachmentDownloadJobPending(id, pending);
}
async function resetAttachmentDownloadPending() {
2021-03-04 16:44:57 -05:00
await Server.resetAttachmentDownloadPending();
}
async function removeAttachmentDownloadJob(id: string) {
2021-03-04 16:44:57 -05:00
await Server.removeAttachmentDownloadJob(id);
}
async function removeAllAttachmentDownloadJobs() {
2021-03-04 16:44:57 -05:00
await Server.removeAllAttachmentDownloadJobs();
}
// Stickers
async function getStickerCount() {
2021-03-04 16:44:57 -05:00
return Server.getStickerCount();
}
async function createOrUpdateStickerPack(pack: StickerPackType) {
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateStickerPack(pack);
}
async function updateStickerPackStatus(
packId: string,
status: StickerPackStatusType,
options?: { timestamp: number }
) {
2021-03-04 16:44:57 -05:00
await Server.updateStickerPackStatus(packId, status, options);
}
async function createOrUpdateSticker(sticker: StickerType) {
2021-03-04 16:44:57 -05:00
await Server.createOrUpdateSticker(sticker);
}
async function updateStickerLastUsed(
packId: string,
stickerId: number,
timestamp: number
) {
2021-03-04 16:44:57 -05:00
await Server.updateStickerLastUsed(packId, stickerId, timestamp);
}
async function addStickerPackReference(messageId: string, packId: string) {
2021-03-04 16:44:57 -05:00
await Server.addStickerPackReference(messageId, packId);
}
async function deleteStickerPackReference(messageId: string, packId: string) {
2021-03-04 16:44:57 -05:00
const paths = await Server.deleteStickerPackReference(messageId, packId);
return paths;
}
async function deleteStickerPack(packId: string) {
2021-03-04 16:44:57 -05:00
const paths = await Server.deleteStickerPack(packId);
return paths;
}
async function getAllStickerPacks() {
2021-03-04 16:44:57 -05:00
const packs = await Server.getAllStickerPacks();
return packs;
}
async function getAllStickers() {
2021-03-04 16:44:57 -05:00
const stickers = await Server.getAllStickers();
return stickers;
}
async function getRecentStickers() {
2021-03-04 16:44:57 -05:00
const recentStickers = await Server.getRecentStickers();
return recentStickers;
}
2021-01-27 14:39:45 -08:00
async function clearAllErrorStickerPackAttempts() {
2021-03-04 16:44:57 -05:00
await Server.clearAllErrorStickerPackAttempts();
2021-01-27 14:39:45 -08:00
}
2019-05-24 16:58:27 -07:00
// Emojis
async function updateEmojiUsage(shortName: string) {
2021-03-04 16:44:57 -05:00
await Server.updateEmojiUsage(shortName);
2019-05-24 16:58:27 -07:00
}
async function getRecentEmojis(limit = 32) {
2021-03-04 16:44:57 -05:00
return Server.getRecentEmojis(limit);
2019-05-24 16:58:27 -07:00
}
2018-10-17 18:01:21 -07:00
// Other
async function removeAll() {
2021-03-04 16:44:57 -05:00
await Server.removeAll();
}
2018-10-17 18:01:21 -07:00
async function removeAllConfiguration() {
2021-03-04 16:44:57 -05:00
await Server.removeAllConfiguration();
2018-10-17 18:01:21 -07:00
}
2018-08-08 10:00:33 -07:00
async function cleanupOrphanedAttachments() {
await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
}
async function ensureFilePermissions() {
await callChannel(ENSURE_FILE_PERMISSIONS);
}
// Note: will need to restart the app after calling this, to set up afresh
async function removeOtherData() {
await Promise.all([
callChannel(ERASE_SQL_KEY),
callChannel(ERASE_ATTACHMENTS_KEY),
callChannel(ERASE_STICKERS_KEY),
callChannel(ERASE_TEMP_KEY),
2019-08-06 17:40:25 -07:00
callChannel(ERASE_DRAFTS_KEY),
]);
}
async function callChannel(name: string) {
return new Promise<void>((resolve, reject) => {
ipcRenderer.send(name);
ipcRenderer.once(`${name}-done`, (_, error) => {
if (error) {
reject(error);
return;
}
resolve();
});
setTimeout(() => {
reject(new Error(`callChannel call to ${name} timed out`));
}, DATABASE_UPDATE_TIMEOUT);
});
}
async function getMessagesNeedingUpgrade(
limit: number,
{ maxVersion = CURRENT_SCHEMA_VERSION }: { maxVersion: number }
) {
2021-03-04 16:44:57 -05:00
const messages = await Server.getMessagesNeedingUpgrade(limit, {
maxVersion,
});
return messages;
}
async function getMessagesWithVisualMediaAttachments(
conversationId: string,
{ limit }: { limit: number }
) {
2021-03-04 16:44:57 -05:00
return Server.getMessagesWithVisualMediaAttachments(conversationId, {
limit,
});
}
async function getMessagesWithFileAttachments(
conversationId: string,
{ limit }: { limit: number }
) {
2021-03-04 16:44:57 -05:00
return Server.getMessagesWithFileAttachments(conversationId, {
limit,
});
}