signal-desktop/ts/sql/Client.ts

1975 lines
52 KiB
TypeScript
Raw Normal View History

2021-01-22 18:17:15 +00:00
// Copyright 2020-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-await-in-loop */
/* eslint-disable camelcase */
2021-10-07 18:16:51 +00:00
import { ipcRenderer as ipc } from 'electron';
import fs from 'fs-extra';
import pify from 'pify';
2022-08-25 05:04:42 +00:00
import PQueue from 'p-queue';
2018-09-21 01:47:19 +00:00
import {
compact,
fromPairs,
groupBy,
isFunction,
isTypedArray,
last,
map,
omit,
toPairs,
uniq,
} from 'lodash';
import { deleteExternalFiles } from '../types/Conversation';
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
2021-09-24 00:49:05 +00:00
import * as Bytes from '../Bytes';
import { CURRENT_SCHEMA_VERSION } from '../types/Message2';
import { createBatcher } from '../util/batcher';
import { assert, softAssert, strictAssert } 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';
import type { ReactionType } from '../types/Reactions';
import type { ConversationColorType, CustomColorType } from '../types/Colors';
2021-10-26 22:59:08 +00:00
import type { UUIDStringType } from '../types/UUID';
2021-11-02 23:01:13 +00:00
import type { BadgeType } from '../badges/types';
2021-08-20 16:06:15 +00:00
import type { ProcessGroupCallRingRequestResult } from '../types/Calling';
import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import * as log from '../logging/log';
import { isValidUuid } from '../types/UUID';
import type { StoredJob } from '../jobs/types';
import { formatJobForInsert } from '../jobs/formatJobForInsert';
import { cleanupMessage } from '../util/cleanup';
import type {
2022-07-28 16:35:29 +00:00
AllItemsType,
AttachmentDownloadJobType,
ClientInterface,
ClientJobType,
ClientSearchResultMessageType,
ConversationType,
2022-07-28 16:35:29 +00:00
ConversationMetricsType,
2021-08-31 21:35:01 +00:00
DeleteSentProtoRecipientOptionsType,
2022-08-15 21:53:33 +00:00
DeleteSentProtoRecipientResultType,
2022-07-28 16:35:29 +00:00
EmojiType,
GetUnreadByConversationAndMarkReadResultType,
GetConversationRangeCenteredOnMessageResultType,
IdentityKeyIdType,
IdentityKeyType,
2022-07-28 16:35:29 +00:00
StoredIdentityKeyType,
ItemKeyType,
ItemType,
2022-07-28 16:35:29 +00:00
StoredItemType,
ConversationMessageStatsType,
MessageType,
MessageTypeUnhydrated,
PreKeyIdType,
PreKeyType,
2022-07-28 16:35:29 +00:00
ReactionResultType,
StoredPreKeyType,
SenderKeyIdType,
SenderKeyType,
SentMessageDBType,
SentMessagesType,
SentProtoType,
SentProtoWithMessageIdsType,
SentRecipientsDBType,
SentRecipientsType,
ServerInterface,
ServerSearchResultMessageType,
SessionIdType,
SessionType,
SignedPreKeyIdType,
SignedPreKeyType,
2022-07-28 16:35:29 +00:00
StoredSignedPreKeyType,
StickerPackStatusType,
2022-08-03 17:10:49 +00:00
StickerPackInfoType,
StickerPackType,
StickerType,
StoryDistributionMemberType,
StoryDistributionType,
StoryDistributionWithMembersType,
StoryReadType,
UnprocessedType,
UnprocessedUpdateType,
2022-08-03 17:10:49 +00:00
UninstalledStickerPackType,
} from './Interface';
2021-03-04 21:44:57 +00:00
import Server from './Server';
import { isCorruptionError } from './errors';
import { MINUTE } from '../util/durations';
import { getMessageIdForLogging } from '../util/idForLogging';
2021-10-07 18:16:51 +00:00
// We listen to a lot of events on ipc, often on the same channel. This prevents
// any warnings that might be sent to the console in that case.
2021-10-07 18:16:51 +00:00
if (ipc && ipc.setMaxListeners) {
ipc.setMaxListeners(0);
} else {
2021-10-07 18:16:51 +00:00
log.warn('sql/Client: ipc is not available!');
}
2021-10-07 18:16:51 +00:00
const getRealPath = pify(fs.realpath);
const MIN_TRACE_DURATION = 10;
const SQL_CHANNEL_KEY = 'sql-channel';
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';
2018-08-08 17:00:33 +00:00
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions';
type ClientJobUpdateType = {
2022-07-28 16:35:29 +00:00
resolve: (value: unknown) => void;
reject: (error: Error) => void;
args?: ReadonlyArray<unknown>;
};
2021-10-07 18:16:51 +00:00
enum RendererState {
InMain = 'InMain',
Opening = 'Opening',
InRenderer = 'InRenderer',
Closing = 'Closing',
}
const _jobs: { [id: string]: ClientJobType } = Object.create(null);
const _DEBUG = false;
let _jobCounter = 0;
let _shuttingDown = false;
2022-07-28 16:35:29 +00:00
let _shutdownCallback: ((error?: Error) => void) | null = null;
let _shutdownPromise: Promise<void> | null = null;
2021-10-07 18:16:51 +00:00
let state = RendererState.InMain;
const startupQueries = new Map<string, number>();
// 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-18 01:01:21 +00:00
createOrUpdateIdentityKey,
getIdentityKeyById,
bulkAddIdentityKeys,
removeIdentityKeyById,
removeAllIdentityKeys,
getAllIdentityKeys,
2018-10-18 01:01:21 +00:00
createOrUpdatePreKey,
getPreKeyById,
bulkAddPreKeys,
removePreKeyById,
2022-07-28 16:35:29 +00:00
removePreKeysByUuid,
2018-10-18 01:01:21 +00:00
removeAllPreKeys,
getAllPreKeys,
2018-10-18 01:01:21 +00:00
createOrUpdateSignedPreKey,
getSignedPreKeyById,
bulkAddSignedPreKeys,
removeSignedPreKeyById,
2022-07-28 16:35:29 +00:00
removeSignedPreKeysByUuid,
2018-10-18 01:01:21 +00:00
removeAllSignedPreKeys,
getAllSignedPreKeys,
2018-10-18 01:01:21 +00:00
createOrUpdateItem,
getItemById,
removeItemById,
removeAllItems,
getAllItems,
2018-10-18 01:01:21 +00:00
createOrUpdateSenderKey,
getSenderKeyById,
removeAllSenderKeys,
getAllSenderKeys,
2021-05-25 22:40:04 +00:00
removeSenderKeyById,
insertSentProto,
deleteSentProtosOlderThan,
deleteSentProtoByMessageId,
insertProtoRecipients,
deleteSentProtoRecipient,
getSentProtoByRecipient,
removeAllSentProtos,
getAllSentProtos,
_getAllSentProtoRecipients,
_getAllSentProtoMessageIds,
2018-10-18 01:01:21 +00:00
createOrUpdateSession,
2019-09-26 19:56:31 +00:00
createOrUpdateSessions,
commitDecryptResult,
2018-10-18 01:01:21 +00:00
bulkAddSessions,
removeSessionById,
removeSessionsByConversation,
2018-10-18 01:01:21 +00:00
removeAllSessions,
getAllSessions,
2018-10-18 01:01:21 +00:00
eraseStorageServiceStateFromConversations,
2018-09-21 01:47:19 +00:00
getConversationCount,
saveConversation,
saveConversations,
getConversationById,
updateConversation,
2019-09-26 19:56:31 +00:00
updateConversations,
2018-09-21 01:47:19 +00:00
removeConversation,
_removeAllConversations,
2021-05-28 16:15:17 +00:00
updateAllConversationColors,
2022-07-08 20:46:25 +00:00
removeAllProfileKeyCredentials,
2018-09-21 01:47:19 +00:00
getAllConversations,
getAllConversationIds,
2021-10-26 22:59:08 +00:00
getAllGroupsInvolvingUuid,
2019-01-14 21:49:58 +00:00
searchMessages,
searchMessagesInConversation,
2018-09-21 01:47:19 +00:00
getMessageCount,
2022-03-29 01:10:08 +00:00
getStoryCount,
saveMessage,
saveMessages,
removeMessage,
2021-01-13 00:42:15 +00:00
removeMessages,
getTotalUnreadForConversation,
getUnreadByConversationAndMarkRead,
getUnreadReactionsAndMarkRead,
markReactionAsRead,
removeReactionFromConversation,
addReaction,
_getAllReactions,
_removeAllReactions,
getMessageBySender,
getMessageById,
getMessagesById,
_getAllMessages,
_removeAllMessages,
getAllMessageIds,
getMessagesBySentAt,
getExpiredMessages,
getMessagesUnexpectedlyMissingExpirationStartTimestamp,
2021-06-16 22:20:17 +00:00
getSoonestMessageExpiry,
getNextTapToViewMessageTimestampToAgeOut,
2019-06-26 19:33:13 +00:00
getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation,
getOlderStories,
getNewerMessagesByConversation,
getMessageMetricsForConversation,
getConversationRangeCenteredOnMessage,
getConversationMessageStats,
getLastConversationMessage,
hasGroupCallHistoryMessage,
migrateConversationMessages,
getUnprocessedCount,
getAllUnprocessedAndIncrementAttempts,
getUnprocessedById,
updateUnprocessedWithData,
2019-09-26 19:56:31 +00:00
updateUnprocessedsWithData,
removeUnprocessed,
removeAllUnprocessed,
getAttachmentDownloadJobById,
getNextAttachmentDownloadJobs,
saveAttachmentDownloadJob,
resetAttachmentDownloadPending,
setAttachmentDownloadJobPending,
removeAttachmentDownloadJob,
removeAllAttachmentDownloadJobs,
createOrUpdateStickerPack,
updateStickerPackStatus,
2022-08-03 17:10:49 +00:00
updateStickerPackInfo,
createOrUpdateSticker,
updateStickerLastUsed,
addStickerPackReference,
deleteStickerPackReference,
getStickerCount,
deleteStickerPack,
getAllStickerPacks,
2022-08-03 17:10:49 +00:00
addUninstalledStickerPack,
removeUninstalledStickerPack,
getInstalledStickerPacks,
getUninstalledStickerPacks,
installStickerPack,
uninstallStickerPack,
getStickerPackInfo,
getAllStickers,
getRecentStickers,
2021-01-27 22:39:45 +00:00
clearAllErrorStickerPackAttempts,
2019-05-24 23:58:27 +00:00
updateEmojiUsage,
getRecentEmojis,
2021-11-02 23:01:13 +00:00
getAllBadges,
updateOrCreateBadges,
badgeImageFileDownloaded,
_getAllStoryDistributions,
_getAllStoryDistributionMembers,
_deleteAllStoryDistributions,
createNewStoryDistribution,
getAllStoryDistributionsWithMembers,
2022-03-04 21:14:52 +00:00
getStoryDistributionWithMembers,
modifyStoryDistribution,
modifyStoryDistributionMembers,
2022-07-01 00:52:03 +00:00
modifyStoryDistributionWithMembers,
deleteStoryDistribution,
_getAllStoryReads,
_deleteAllStoryReads,
addNewStoryRead,
getLastStoryReadsForAuthor,
2022-03-29 01:10:08 +00:00
countStoryReadsByConversation,
removeAll,
2018-10-18 01:01:21 +00:00
removeAllConfiguration,
getMessagesNeedingUpgrade,
getMessagesWithVisualMediaAttachments,
getMessagesWithFileAttachments,
getMessageServerGuidsForSpam,
2021-04-29 23:02:27 +00:00
getJobsInQueue,
insertJob,
deleteJob,
2021-08-20 16:06:15 +00:00
processGroupCallRingRequest,
processGroupCallRingCancelation,
cleanExpiredGroupCallRings,
2021-09-15 18:45:22 +00:00
getMaxMessageCounter,
getStatisticsForLogging,
// Client-side only
shutdown,
removeAllMessagesInConversation,
removeOtherData,
2018-08-08 17:00:33 +00:00
cleanupOrphanedAttachments,
ensureFilePermissions,
// Client-side only, and test-only
2021-10-07 18:16:51 +00:00
startInRendererProcess,
goBackToMainProcess,
_jobs,
};
export default dataInterface;
2021-10-07 18:16:51 +00:00
async function startInRendererProcess(isTesting = false): Promise<void> {
strictAssert(
state === RendererState.InMain,
`startInRendererProcess: expected ${state} to be ${RendererState.InMain}`
);
log.info('data.startInRendererProcess: switching to renderer process');
state = RendererState.Opening;
if (!isTesting) {
ipc.send('database-ready');
await new Promise<void>(resolve => {
ipc.once('database-ready', () => {
resolve();
});
});
}
2021-10-07 18:16:51 +00:00
const configDir = await getRealPath(ipc.sendSync('get-user-data-path'));
const key = ipc.sendSync('user-config-key');
await Server.initializeRenderer({ configDir, key });
log.info('data.startInRendererProcess: switched to renderer process');
state = RendererState.InRenderer;
}
async function goBackToMainProcess(): Promise<void> {
2021-10-15 18:43:13 +00:00
if (state === RendererState.InMain) {
log.info('goBackToMainProcess: Already in the main process');
return;
}
2021-10-07 18:16:51 +00:00
strictAssert(
state === RendererState.InRenderer,
`goBackToMainProcess: expected ${state} to be ${RendererState.InRenderer}`
);
// We don't need to wait for pending queries since they are synchronous.
log.info('data.goBackToMainProcess: switching to main process');
const closePromise = close();
// It should be the last query we run in renderer process
2021-10-07 18:16:51 +00:00
state = RendererState.Closing;
await closePromise;
2021-10-07 18:16:51 +00:00
state = RendererState.InMain;
// Print query statistics for whole startup
const entries = Array.from(startupQueries.entries());
startupQueries.clear();
// Sort by decreasing duration
entries
.sort((a, b) => b[1] - a[1])
.filter(([_, duration]) => duration > MIN_TRACE_DURATION)
.forEach(([query, duration]) => {
log.info(`startup query: ${query} ${duration}ms`);
});
2021-10-07 18:16:51 +00:00
log.info('data.goBackToMainProcess: switched to main process');
}
const channelsAsUnknown = fromPairs(
compact(
2022-07-28 16:35:29 +00:00
map(toPairs(dataInterface), ([name, value]: [string, unknown]) => {
if (isFunction(value)) {
2021-04-07 22:40:12 +00:00
return [name, makeChannel(name)];
}
return null;
})
)
2022-07-28 16:35:29 +00:00
) as unknown;
2022-07-28 16:35:29 +00:00
const channels: ServerInterface = channelsAsUnknown as ServerInterface;
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: MessageType): 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) {
assert(false, 'received_at was not set on the message');
2022-07-28 16:35:29 +00:00
result.received_at = window.Signal.Util.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']);
}
return attachment;
});
}
2022-07-28 16:35:29 +00:00
return _cleanData(omit(result, ['dataMessage']));
}
async function _shutdown() {
const jobKeys = Object.keys(_jobs);
log.info(
`data.shutdown: shutdown requested. ${jobKeys.length} jobs outstanding`
);
if (_shutdownPromise) {
await _shutdownPromise;
return;
}
_shuttingDown = true;
// No outstanding jobs, return immediately
if (jobKeys.length === 0 || _DEBUG) {
return;
}
// Outstanding jobs; we need to wait until the last one is done
_shutdownPromise = new Promise<void>((resolve, reject) => {
2022-07-28 16:35:29 +00:00
_shutdownCallback = (error?: Error) => {
log.info('data.shutdown: process complete');
if (error) {
reject(error);
return;
}
resolve();
};
});
await _shutdownPromise;
}
function _makeJob(fnName: string) {
if (_shuttingDown && fnName !== 'close') {
throw new Error(
`Rejecting SQL channel job (${fnName}); application is shutting down`
);
}
_jobCounter += 1;
const id = _jobCounter;
if (_DEBUG) {
log.info(`SQL channel job ${id} (${fnName}) started`);
}
_jobs[id] = {
fnName,
start: Date.now(),
};
return id;
}
function _updateJob(id: number, data: ClientJobUpdateType) {
const { resolve, reject } = data;
const { fnName, start } = _jobs[id];
_jobs[id] = {
..._jobs[id],
...data,
2022-07-28 16:35:29 +00:00
resolve: (value: unknown) => {
_removeJob(id);
const end = Date.now();
if (_DEBUG) {
log.info(
`SQL channel job ${id} (${fnName}) succeeded in ${end - start}ms`
);
}
return resolve(value);
},
reject: (error: Error) => {
_removeJob(id);
const end = Date.now();
log.info(`SQL channel job ${id} (${fnName}) failed in ${end - start}ms`);
return reject(error);
},
};
}
function _removeJob(id: number) {
if (_DEBUG) {
_jobs[id].complete = true;
return;
}
delete _jobs[id];
if (_shutdownCallback) {
const keys = Object.keys(_jobs);
if (keys.length === 0) {
_shutdownCallback();
}
}
}
function _getJob(id: number) {
return _jobs[id];
}
2021-10-07 18:16:51 +00:00
if (ipc && ipc.on) {
ipc.on(`${SQL_CHANNEL_KEY}-done`, (_, jobId, errorForDisplay, result) => {
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received SQL channel reply to job ${jobId}, but did not have it in our registry!`
);
}
2021-10-07 18:16:51 +00:00
const { resolve, reject, fnName } = job;
2021-10-07 18:16:51 +00:00
if (!resolve || !reject) {
throw new Error(
`SQL channel job ${jobId} (${fnName}): didn't have a resolve or reject`
);
}
2021-10-07 18:16:51 +00:00
if (errorForDisplay) {
return reject(
new Error(
`Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
2021-10-07 18:16:51 +00:00
return resolve(result);
});
} else {
2021-10-07 18:16:51 +00:00
log.warn('sql/Client: ipc.on is not available!');
}
function makeChannel(fnName: string) {
2022-07-28 16:35:29 +00:00
return async (...args: ReadonlyArray<unknown>) => {
// During startup we want to avoid the high overhead of IPC so we utilize
// the db that exists in the renderer process to be able to boot up quickly
// once the app is running we switch back to the main process to avoid the
// UI from locking up whenever we do costly db operations.
2021-10-07 18:16:51 +00:00
if (state === RendererState.InRenderer) {
const serverFnName = fnName as keyof ServerInterface;
2022-07-28 16:35:29 +00:00
const serverFn = Server[serverFnName] as (
...fnArgs: ReadonlyArray<unknown>
) => unknown;
const start = Date.now();
try {
// Ignoring this error TS2556: Expected 3 arguments, but got 0 or more.
2022-07-28 16:35:29 +00:00
return await serverFn(...args);
} catch (error) {
if (isCorruptionError(error)) {
log.error(
'Detected sql corruption in renderer process. ' +
`Restarting the application immediately. Error: ${error.message}`
);
2022-04-07 22:49:23 +00:00
ipc?.send('database-error', error.stack);
}
log.error(
`Renderer SQL channel job (${fnName}) error ${error.message}`
);
throw error;
} finally {
const duration = Date.now() - start;
startupQueries.set(
serverFnName,
(startupQueries.get(serverFnName) || 0) + duration
);
if (duration > MIN_TRACE_DURATION || _DEBUG) {
log.info(
`Renderer SQL channel job (${fnName}) completed in ${duration}ms`
);
}
}
}
const jobId = _makeJob(fnName);
return createTaskWithTimeout(
() =>
new Promise((resolve, reject) => {
try {
2021-10-07 18:16:51 +00:00
ipc.send(SQL_CHANNEL_KEY, jobId, fnName, ...args);
_updateJob(jobId, {
resolve,
reject,
args: _DEBUG ? args : undefined,
});
} catch (error) {
_removeJob(jobId);
reject(error);
}
}),
`SQL channel job ${jobId} (${fnName})`
)();
};
}
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 _shutdown();
// Close database
await close();
}
// Note: will need to restart the app after calling this, to set up afresh
2022-07-28 16:35:29 +00:00
async function close(): Promise<void> {
await channels.close();
}
// Note: will need to restart the app after calling this, to set up afresh
2022-07-28 16:35:29 +00:00
async function removeDB(): Promise<void> {
await channels.removeDB();
}
2022-07-28 16:35:29 +00:00
async function removeIndexedDBFiles(): Promise<void> {
await channels.removeIndexedDBFiles();
}
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);
await channels.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> {
const data = await channels.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)
);
await channels.bulkAddIdentityKeys(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeIdentityKeyById(id: IdentityKeyIdType): Promise<void> {
await channels.removeIdentityKeyById(id);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeAllIdentityKeys(): Promise<void> {
await channels.removeAllIdentityKeys();
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllIdentityKeys(): Promise<Array<IdentityKeyType>> {
const keys = await channels.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
// 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);
await channels.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> {
const data = await channels.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)
);
await channels.bulkAddPreKeys(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removePreKeyById(id: PreKeyIdType): Promise<void> {
await channels.removePreKeyById(id);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> {
await channels.removePreKeysByUuid(uuid);
}
async function removeAllPreKeys(): Promise<void> {
await channels.removeAllPreKeys();
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllPreKeys(): Promise<Array<PreKeyType>> {
const keys = await channels.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);
await channels.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> {
const data = await channels.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>> {
const keys = await channels.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)
);
await channels.bulkAddSignedPreKeys(updated);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise<void> {
await channels.removeSignedPreKeyById(id);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
await channels.removeSignedPreKeysByUuid(uuid);
}
async function removeAllSignedPreKeys(): Promise<void> {
await channels.removeAllSignedPreKeys();
2018-10-18 01:01:21 +00:00
}
// Items
2022-07-28 16:35:29 +00:00
const ITEM_SPECS: Partial<Record<ItemKeyType, ObjectMappingSpecType>> = {
identityKeyMap: {
key: 'value',
valueSpec: {
isMap: true,
valueSpec: ['privKey', 'pubKey'],
},
},
profileKey: ['value'],
senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'],
subscriberId: ['value'],
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
await channels.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];
const data = await channels.getItemById(id);
2018-10-18 01:01:21 +00:00
2022-07-28 16:35:29 +00:00
return spec ? specToBytes(spec, data) : (data as unknown as ItemType<K>);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllItems(): Promise<AllItemsType> {
const items = await channels.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];
2022-07-28 16:35:29 +00:00
const deserializedValue = keys
? (specToBytes(keys, { value }) as ItemType<typeof key>).value
: value;
result[key] = deserializedValue;
}
return result;
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeItemById(id: ItemKeyType): Promise<void> {
await channels.removeItemById(id);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeAllItems(): Promise<void> {
await channels.removeAllItems();
2018-10-18 01:01:21 +00:00
}
// Sender Keys
async function createOrUpdateSenderKey(key: SenderKeyType): Promise<void> {
await channels.createOrUpdateSenderKey(key);
}
async function getSenderKeyById(
id: SenderKeyIdType
): Promise<SenderKeyType | undefined> {
return channels.getSenderKeyById(id);
}
async function removeAllSenderKeys(): Promise<void> {
await channels.removeAllSenderKeys();
}
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
return channels.getAllSenderKeys();
}
async function removeSenderKeyById(id: SenderKeyIdType): Promise<void> {
2021-05-25 22:40:04 +00:00
return channels.removeSenderKeyById(id);
}
// Sent Protos
async function insertSentProto(
proto: SentProtoType,
options: {
messageIds: SentMessagesType;
recipients: SentRecipientsType;
}
): Promise<number> {
return channels.insertSentProto(proto, {
...options,
messageIds: uniq(options.messageIds),
});
}
async function deleteSentProtosOlderThan(timestamp: number): Promise<void> {
await channels.deleteSentProtosOlderThan(timestamp);
}
async function deleteSentProtoByMessageId(messageId: string): Promise<void> {
await channels.deleteSentProtoByMessageId(messageId);
}
async function insertProtoRecipients(options: {
id: number;
recipientUuid: string;
deviceIds: Array<number>;
}): Promise<void> {
await channels.insertProtoRecipients(options);
}
2021-08-31 21:35:01 +00:00
async function deleteSentProtoRecipient(
options:
| DeleteSentProtoRecipientOptionsType
| ReadonlyArray<DeleteSentProtoRecipientOptionsType>
2022-08-15 21:53:33 +00:00
): Promise<DeleteSentProtoRecipientResultType> {
return channels.deleteSentProtoRecipient(options);
}
async function getSentProtoByRecipient(options: {
now: number;
recipientUuid: string;
timestamp: number;
}): Promise<SentProtoWithMessageIdsType | undefined> {
return channels.getSentProtoByRecipient(options);
}
async function removeAllSentProtos(): Promise<void> {
await channels.removeAllSentProtos();
}
async function getAllSentProtos(): Promise<Array<SentProtoType>> {
return channels.getAllSentProtos();
}
// Test-only:
async function _getAllSentProtoRecipients(): Promise<
Array<SentRecipientsDBType>
> {
return channels._getAllSentProtoRecipients();
}
async function _getAllSentProtoMessageIds(): Promise<Array<SentMessageDBType>> {
return channels._getAllSentProtoMessageIds();
}
2018-10-18 01:01:21 +00:00
// Sessions
2022-07-28 16:35:29 +00:00
async function createOrUpdateSession(data: SessionType): Promise<void> {
await channels.createOrUpdateSession(data);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function createOrUpdateSessions(
array: Array<SessionType>
): Promise<void> {
await channels.createOrUpdateSessions(array);
2019-09-26 19:56:31 +00:00
}
async function commitDecryptResult(options: {
senderKeys: Array<SenderKeyType>;
sessions: Array<SessionType>;
unprocessed: Array<UnprocessedType>;
2022-07-28 16:35:29 +00:00
}): Promise<void> {
await channels.commitDecryptResult(options);
}
2022-07-28 16:35:29 +00:00
async function bulkAddSessions(array: Array<SessionType>): Promise<void> {
await channels.bulkAddSessions(array);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeSessionById(id: SessionIdType): Promise<void> {
await channels.removeSessionById(id);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeSessionsByConversation(
conversationId: string
): Promise<void> {
await channels.removeSessionsByConversation(conversationId);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeAllSessions(): Promise<void> {
await channels.removeAllSessions();
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllSessions(): Promise<Array<SessionType>> {
const sessions = await channels.getAllSessions();
return sessions;
}
2018-10-18 01:01:21 +00:00
// Conversation
2022-07-28 16:35:29 +00:00
async function getConversationCount(): Promise<number> {
return channels.getConversationCount();
2018-09-21 01:47:19 +00:00
}
2022-07-28 16:35:29 +00:00
async function saveConversation(data: ConversationType): Promise<void> {
await channels.saveConversation(data);
2018-09-21 01:47:19 +00:00
}
2022-07-28 16:35:29 +00:00
async function saveConversations(
array: Array<ConversationType>
): Promise<void> {
await channels.saveConversations(array);
2018-09-21 01:47:19 +00:00
}
2022-07-28 16:35:29 +00:00
async function getConversationById(
id: string
): Promise<ConversationType | undefined> {
return channels.getConversationById(id);
2018-09-21 01:47:19 +00:00
}
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]);
assert(maybeLast !== undefined, 'Empty array in `groupBy` result');
return maybeLast;
});
2018-09-21 01:47:19 +00:00
2019-09-26 19:56:31 +00:00
await updateConversations(mostRecent);
},
});
2022-07-28 16:35:29 +00:00
function updateConversation(data: ConversationType): void {
2019-09-26 19:56:31 +00:00
updateConversationBatcher.add(data);
}
2022-07-28 16:35:29 +00:00
async function updateConversations(
array: Array<ConversationType>
): Promise<void> {
const { cleaned, pathsChanged } = cleanDataForIpc(array);
assert(
!pathsChanged.length,
`Paths were cleaned: ${JSON.stringify(pathsChanged)}`
);
await channels.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> {
const existing = await 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) {
await channels.removeConversation(id);
await deleteExternalFiles(existing, {
deleteAttachmentData: window.Signal.Migrations.deleteAttachmentData,
});
2018-09-21 01:47:19 +00:00
}
}
async function _removeAllConversations(): Promise<void> {
await channels._removeAllConversations();
}
2022-07-28 16:35:29 +00:00
async function eraseStorageServiceStateFromConversations(): Promise<void> {
await channels.eraseStorageServiceStateFromConversations();
}
2022-07-28 16:35:29 +00:00
async function getAllConversations(): Promise<Array<ConversationType>> {
return channels.getAllConversations();
2018-09-21 01:47:19 +00:00
}
2022-07-28 16:35:29 +00:00
async function getAllConversationIds(): Promise<Array<string>> {
const ids = await channels.getAllConversationIds();
2018-09-21 01:47:19 +00:00
return ids;
}
2022-07-28 16:35:29 +00:00
async function getAllGroupsInvolvingUuid(
uuid: UUIDStringType
): Promise<Array<ConversationType>> {
return channels.getAllGroupsInvolvingUuid(uuid);
2018-09-21 01:47:19 +00:00
}
function handleSearchMessageJSON(
messages: Array<ServerSearchResultMessageType>
): Array<ClientSearchResultMessageType> {
return messages.map(message => ({
json: message.json,
// Empty array is a default value. `message.json` has the real field
bodyRanges: [],
...JSON.parse(message.json),
snippet: message.snippet,
2019-08-09 23:12:29 +00:00
}));
}
async function searchMessages(
query: string,
{ limit }: { limit?: number } = {}
2022-07-28 16:35:29 +00:00
): Promise<Array<ClientSearchResultMessageType>> {
const messages = await channels.searchMessages(query, { limit });
return handleSearchMessageJSON(messages);
2019-01-14 21:49:58 +00:00
}
async function searchMessagesInConversation(
query: string,
conversationId: string,
{ limit }: { limit?: number } = {}
2022-07-28 16:35:29 +00:00
): Promise<Array<ClientSearchResultMessageType>> {
const messages = await channels.searchMessagesInConversation(
2019-01-14 21:49:58 +00:00
query,
conversationId,
{ limit }
);
return handleSearchMessageJSON(messages);
2018-09-21 01:47:19 +00:00
}
2018-10-18 01:01:21 +00:00
// Message
2022-07-28 16:35:29 +00:00
async function getMessageCount(conversationId?: string): Promise<number> {
return channels.getMessageCount(conversationId);
}
2022-07-28 16:35:29 +00:00
async function getStoryCount(conversationId: string): Promise<number> {
2022-03-29 01:10:08 +00:00
return channels.getStoryCount(conversationId);
}
async function saveMessage(
data: MessageType,
2021-12-20 21:04:02 +00:00
options: {
jobToInsert?: Readonly<StoredJob>;
forceSave?: boolean;
ourUuid: UUIDStringType;
}
2022-07-28 16:35:29 +00:00
): Promise<string> {
const id = await channels.saveMessage(_cleanMessageData(data), {
...options,
jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert),
});
softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
expiringMessagesDeletionService.update();
tapToViewMessagesDeletionService.update();
2018-10-18 01:01:21 +00:00
return id;
}
async function saveMessages(
arrayOfMessages: ReadonlyArray<MessageType>,
2021-12-20 21:04:02 +00:00
options: { forceSave?: boolean; ourUuid: UUIDStringType }
2022-07-28 16:35:29 +00:00
): Promise<void> {
await channels.saveMessages(
arrayOfMessages.map(message => _cleanMessageData(message)),
options
);
expiringMessagesDeletionService.update();
tapToViewMessagesDeletionService.update();
}
2022-07-28 16:35:29 +00:00
async function removeMessage(id: string): Promise<void> {
const message = await 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) {
await channels.removeMessage(id);
await cleanupMessage(message);
}
}
// Note: this method will not clean up external files, just delete from SQL
2022-07-28 16:35:29 +00:00
async function removeMessages(ids: Array<string>): Promise<void> {
await channels.removeMessages(ids);
}
2022-07-28 16:35:29 +00:00
async function getMessageById(id: string): Promise<MessageType | undefined> {
return channels.getMessageById(id);
2018-09-21 01:47:19 +00:00
}
2022-07-28 16:35:29 +00:00
async function getMessagesById(
messageIds: Array<string>
): Promise<Array<MessageType>> {
if (!messageIds.length) {
return [];
}
return channels.getMessagesById(messageIds);
}
2018-09-21 01:47:19 +00:00
// For testing only
2022-07-28 16:35:29 +00:00
async function _getAllMessages(): Promise<Array<MessageType>> {
return channels._getAllMessages();
}
2022-07-28 16:35:29 +00:00
async function _removeAllMessages(): Promise<void> {
await channels._removeAllMessages();
}
2022-07-28 16:35:29 +00:00
async function getAllMessageIds(): Promise<Array<string>> {
const ids = await channels.getAllMessageIds();
return ids;
}
async function getMessageBySender({
source,
sourceUuid,
sourceDevice,
sent_at,
}: {
source: string;
2022-07-08 20:46:25 +00:00
sourceUuid: UUIDStringType;
sourceDevice: number;
sent_at: number;
2022-07-28 16:35:29 +00:00
}): Promise<MessageType | undefined> {
return channels.getMessageBySender({
source,
sourceUuid,
sourceDevice,
sent_at,
});
}
async function getTotalUnreadForConversation(
conversationId: string,
options: {
storyId: UUIDStringType | undefined;
isGroup: boolean;
}
2022-07-28 16:35:29 +00:00
): Promise<number> {
return channels.getTotalUnreadForConversation(conversationId, options);
}
async function getUnreadByConversationAndMarkRead(options: {
conversationId: string;
2022-04-20 23:33:38 +00:00
isGroup?: boolean;
newestUnreadAt: number;
readAt?: number;
storyId?: UUIDStringType;
2022-07-28 16:35:29 +00:00
}): Promise<GetUnreadByConversationAndMarkReadResultType> {
return channels.getUnreadByConversationAndMarkRead(options);
}
async function getUnreadReactionsAndMarkRead(options: {
conversationId: string;
newestUnreadAt: number;
storyId?: UUIDStringType;
2022-07-28 16:35:29 +00:00
}): Promise<Array<ReactionResultType>> {
return channels.getUnreadReactionsAndMarkRead(options);
}
async function markReactionAsRead(
targetAuthorUuid: string,
targetTimestamp: number
2022-07-28 16:35:29 +00:00
): Promise<ReactionType | undefined> {
return channels.markReactionAsRead(targetAuthorUuid, targetTimestamp);
}
async function removeReactionFromConversation(reaction: {
emoji: string;
fromId: string;
targetAuthorUuid: string;
targetTimestamp: number;
2022-07-28 16:35:29 +00:00
}): Promise<void> {
return channels.removeReactionFromConversation(reaction);
}
2022-07-28 16:35:29 +00:00
async function addReaction(reactionObj: ReactionType): Promise<void> {
return channels.addReaction(reactionObj);
}
2022-07-28 16:35:29 +00:00
async function _getAllReactions(): Promise<Array<ReactionType>> {
return channels._getAllReactions();
}
2022-07-28 16:35:29 +00:00
async function _removeAllReactions(): Promise<void> {
await channels._removeAllReactions();
}
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 getOlderMessagesByConversation(
conversationId: string,
{
2022-04-20 23:33:38 +00:00
isGroup,
limit = 100,
messageId,
receivedAt = Number.MAX_VALUE,
sentAt = Number.MAX_VALUE,
storyId,
}: {
isGroup: boolean;
limit?: number;
messageId?: string;
receivedAt?: number;
sentAt?: number;
storyId: string | undefined;
}
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
const messages = await channels.getOlderMessagesByConversation(
conversationId,
{
2022-04-20 23:33:38 +00:00
isGroup,
limit,
receivedAt,
sentAt,
messageId,
storyId,
}
);
return handleMessageJSON(messages);
}
async function getOlderStories(options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
2022-07-08 20:46:25 +00:00
sourceUuid?: UUIDStringType;
}): Promise<Array<MessageType>> {
return channels.getOlderStories(options);
}
async function getNewerMessagesByConversation(
conversationId: string,
{
2022-04-20 23:33:38 +00:00
isGroup,
limit = 100,
receivedAt = 0,
sentAt = 0,
storyId,
}: {
isGroup: boolean;
limit?: number;
receivedAt?: number;
sentAt?: number;
storyId: UUIDStringType | undefined;
}
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
const messages = await channels.getNewerMessagesByConversation(
conversationId,
{
2022-04-20 23:33:38 +00:00
isGroup,
limit,
receivedAt,
sentAt,
storyId,
}
);
return handleMessageJSON(messages);
}
async function getConversationMessageStats({
conversationId,
2022-04-20 23:33:38 +00:00
isGroup,
2021-10-26 22:59:08 +00:00
ourUuid,
}: {
conversationId: string;
2022-04-20 23:33:38 +00:00
isGroup?: boolean;
2021-10-26 22:59:08 +00:00
ourUuid: UUIDStringType;
}): Promise<ConversationMessageStatsType> {
2021-11-11 22:43:05 +00:00
const { preview, activity, hasUserInitiatedMessages } =
await channels.getConversationMessageStats({
2021-11-11 22:43:05 +00:00
conversationId,
2022-04-20 23:33:38 +00:00
isGroup,
2021-11-11 22:43:05 +00:00
ourUuid,
});
return {
preview,
activity,
hasUserInitiatedMessages,
};
2020-08-07 00:50:54 +00:00
}
async function getLastConversationMessage({
conversationId,
}: {
conversationId: string;
2022-07-28 16:35:29 +00:00
}): Promise<MessageType | undefined> {
return channels.getLastConversationMessage({ conversationId });
}
async function getMessageMetricsForConversation(
conversationId: string,
2022-04-20 23:33:38 +00:00
storyId?: UUIDStringType,
isGroup?: boolean
2022-07-28 16:35:29 +00:00
): Promise<ConversationMetricsType> {
const result = await channels.getMessageMetricsForConversation(
conversationId,
2022-04-20 23:33:38 +00:00
storyId,
isGroup
);
return result;
}
async function getConversationRangeCenteredOnMessage(options: {
conversationId: string;
isGroup: boolean;
limit?: number;
messageId: string;
receivedAt: number;
sentAt?: number;
storyId: UUIDStringType | undefined;
2022-07-28 16:35:29 +00:00
}): Promise<GetConversationRangeCenteredOnMessageResultType<MessageType>> {
const result = await channels.getConversationRangeCenteredOnMessage(options);
return {
...result,
older: handleMessageJSON(result.older),
newer: handleMessageJSON(result.newer),
};
}
function hasGroupCallHistoryMessage(
conversationId: string,
eraId: string
): Promise<boolean> {
return channels.hasGroupCallHistoryMessage(conversationId, eraId);
}
async function migrateConversationMessages(
obsoleteId: string,
currentId: string
2022-07-28 16:35:29 +00:00
): Promise<void> {
await channels.migrateConversationMessages(obsoleteId, currentId);
}
async function removeAllMessagesInConversation(
conversationId: string,
{
2021-01-13 00:42:15 +00:00
logId,
}: {
logId: string;
}
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.
messages = await getOlderMessagesByConversation(conversationId, {
2021-01-13 00:42:15 +00:00
limit: chunkSize,
isGroup: true,
storyId: undefined,
});
if (!messages.length) {
return;
}
const ids = messages.map(message => message.id);
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.
2022-08-25 05:04:42 +00:00
const queue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
2021-01-13 00:42:15 +00:00
queue.addAll(
messages.map(
(message: MessageType) => async () => cleanupMessage(message)
)
);
2021-01-13 00:42:15 +00:00
await queue.onIdle();
log.info(`removeAllMessagesInConversation/${logId}: Deleting...`);
await channels.removeMessages(ids);
} while (messages.length > 0);
}
2022-07-28 16:35:29 +00:00
async function getMessagesBySentAt(
sentAt: number
): Promise<Array<MessageType>> {
return channels.getMessagesBySentAt(sentAt);
}
2022-07-28 16:35:29 +00:00
async function getExpiredMessages(): Promise<Array<MessageType>> {
return channels.getExpiredMessages();
}
2022-07-28 16:35:29 +00:00
function getMessagesUnexpectedlyMissingExpirationStartTimestamp(): Promise<
Array<MessageType>
> {
return channels.getMessagesUnexpectedlyMissingExpirationStartTimestamp();
}
2022-07-28 16:35:29 +00:00
function getSoonestMessageExpiry(): Promise<number | undefined> {
2021-06-16 22:20:17 +00:00
return channels.getSoonestMessageExpiry();
}
2022-07-28 16:35:29 +00:00
async function getNextTapToViewMessageTimestampToAgeOut(): Promise<
number | undefined
> {
return channels.getNextTapToViewMessageTimestampToAgeOut();
2019-06-26 19:33:13 +00:00
}
2022-07-28 16:35:29 +00:00
async function getTapToViewMessagesNeedingErase(): Promise<Array<MessageType>> {
return channels.getTapToViewMessagesNeedingErase();
2019-06-26 19:33:13 +00:00
}
2018-10-18 01:01:21 +00:00
// Unprocessed
2022-07-28 16:35:29 +00:00
async function getUnprocessedCount(): Promise<number> {
return channels.getUnprocessedCount();
}
2022-07-28 16:35:29 +00:00
async function getAllUnprocessedAndIncrementAttempts(): Promise<
Array<UnprocessedType>
> {
return channels.getAllUnprocessedAndIncrementAttempts();
}
2022-07-28 16:35:29 +00:00
async function getUnprocessedById(
id: string
): Promise<UnprocessedType | undefined> {
return channels.getUnprocessedById(id);
}
async function updateUnprocessedWithData(
id: string,
data: UnprocessedUpdateType
2022-07-28 16:35:29 +00:00
): Promise<void> {
await channels.updateUnprocessedWithData(id, data);
}
async function updateUnprocessedsWithData(
array: Array<{ id: string; data: UnprocessedUpdateType }>
2022-07-28 16:35:29 +00:00
): Promise<void> {
await channels.updateUnprocessedsWithData(array);
2019-09-26 19:56:31 +00:00
}
2022-07-28 16:35:29 +00:00
async function removeUnprocessed(id: string | Array<string>): Promise<void> {
await channels.removeUnprocessed(id);
}
2022-07-28 16:35:29 +00:00
async function removeAllUnprocessed(): Promise<void> {
await channels.removeAllUnprocessed();
}
// Attachment downloads
2022-07-28 16:35:29 +00:00
async function getAttachmentDownloadJobById(
id: string
): Promise<AttachmentDownloadJobType | undefined> {
return channels.getAttachmentDownloadJobById(id);
}
async function getNextAttachmentDownloadJobs(
limit?: number,
options?: { timestamp?: number }
2022-07-28 16:35:29 +00:00
): Promise<Array<AttachmentDownloadJobType>> {
return channels.getNextAttachmentDownloadJobs(limit, options);
}
2022-07-28 16:35:29 +00:00
async function saveAttachmentDownloadJob(
job: AttachmentDownloadJobType
): Promise<void> {
await channels.saveAttachmentDownloadJob(_cleanData(job));
}
2022-07-28 16:35:29 +00:00
async function setAttachmentDownloadJobPending(
id: string,
pending: boolean
): Promise<void> {
await channels.setAttachmentDownloadJobPending(id, pending);
}
2022-07-28 16:35:29 +00:00
async function resetAttachmentDownloadPending(): Promise<void> {
await channels.resetAttachmentDownloadPending();
}
2022-07-28 16:35:29 +00:00
async function removeAttachmentDownloadJob(id: string): Promise<void> {
await channels.removeAttachmentDownloadJob(id);
}
2022-07-28 16:35:29 +00:00
async function removeAllAttachmentDownloadJobs(): Promise<void> {
await channels.removeAllAttachmentDownloadJobs();
}
// Stickers
2022-07-28 16:35:29 +00:00
async function getStickerCount(): Promise<number> {
return channels.getStickerCount();
}
2022-07-28 16:35:29 +00:00
async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
await channels.createOrUpdateStickerPack(pack);
}
async function updateStickerPackStatus(
packId: string,
status: StickerPackStatusType,
options?: { timestamp: number }
2022-07-28 16:35:29 +00:00
): Promise<void> {
await channels.updateStickerPackStatus(packId, status, options);
}
2022-08-03 17:10:49 +00:00
async function updateStickerPackInfo(info: StickerPackInfoType): Promise<void> {
await channels.updateStickerPackInfo(info);
}
2022-07-28 16:35:29 +00:00
async function createOrUpdateSticker(sticker: StickerType): Promise<void> {
await channels.createOrUpdateSticker(sticker);
}
async function updateStickerLastUsed(
packId: string,
stickerId: number,
timestamp: number
2022-07-28 16:35:29 +00:00
): Promise<void> {
2022-08-03 17:10:49 +00:00
return channels.updateStickerLastUsed(packId, stickerId, timestamp);
}
2022-07-28 16:35:29 +00:00
async function addStickerPackReference(
messageId: string,
packId: string
): Promise<void> {
await channels.addStickerPackReference(messageId, packId);
}
2022-07-28 16:35:29 +00:00
async function deleteStickerPackReference(
messageId: string,
packId: string
): Promise<ReadonlyArray<string> | undefined> {
return channels.deleteStickerPackReference(messageId, packId);
}
2022-07-28 16:35:29 +00:00
async function deleteStickerPack(packId: string): Promise<Array<string>> {
2022-08-03 17:10:49 +00:00
return channels.deleteStickerPack(packId);
}
2022-07-28 16:35:29 +00:00
async function getAllStickerPacks(): Promise<Array<StickerPackType>> {
const packs = await channels.getAllStickerPacks();
return packs;
}
2022-08-03 17:10:49 +00:00
async function addUninstalledStickerPack(
pack: UninstalledStickerPackType
): Promise<void> {
return channels.addUninstalledStickerPack(pack);
}
async function removeUninstalledStickerPack(packId: string): Promise<void> {
return channels.removeUninstalledStickerPack(packId);
}
async function getInstalledStickerPacks(): Promise<Array<StickerPackType>> {
return channels.getInstalledStickerPacks();
}
async function getUninstalledStickerPacks(): Promise<
Array<UninstalledStickerPackType>
> {
return channels.getUninstalledStickerPacks();
}
async function installStickerPack(
packId: string,
timestamp: number
): Promise<void> {
return channels.installStickerPack(packId, timestamp);
}
async function uninstallStickerPack(
packId: string,
timestamp: number
): Promise<void> {
return channels.uninstallStickerPack(packId, timestamp);
}
async function getStickerPackInfo(
packId: string
): Promise<StickerPackInfoType | undefined> {
return channels.getStickerPackInfo(packId);
}
2022-07-28 16:35:29 +00:00
async function getAllStickers(): Promise<Array<StickerType>> {
const stickers = await channels.getAllStickers();
return stickers;
}
2022-07-28 16:35:29 +00:00
async function getRecentStickers(): Promise<Array<StickerType>> {
const recentStickers = await channels.getRecentStickers();
return recentStickers;
}
2022-07-28 16:35:29 +00:00
async function clearAllErrorStickerPackAttempts(): Promise<void> {
await channels.clearAllErrorStickerPackAttempts();
2021-01-27 22:39:45 +00:00
}
2019-05-24 23:58:27 +00:00
// Emojis
2022-07-28 16:35:29 +00:00
async function updateEmojiUsage(shortName: string): Promise<void> {
await channels.updateEmojiUsage(shortName);
2019-05-24 23:58:27 +00:00
}
2022-07-28 16:35:29 +00:00
async function getRecentEmojis(limit = 32): Promise<Array<EmojiType>> {
return channels.getRecentEmojis(limit);
2019-05-24 23:58:27 +00:00
}
2021-11-02 23:01:13 +00:00
// Badges
function getAllBadges(): Promise<Array<BadgeType>> {
return channels.getAllBadges();
}
async function updateOrCreateBadges(
badges: ReadonlyArray<BadgeType>
): Promise<void> {
if (badges.length) {
await channels.updateOrCreateBadges(badges);
}
}
function badgeImageFileDownloaded(
url: string,
localPath: string
): Promise<void> {
return channels.badgeImageFileDownloaded(url, localPath);
}
// Story Distributions
async function _getAllStoryDistributions(): Promise<
Array<StoryDistributionType>
> {
return channels._getAllStoryDistributions();
}
async function _getAllStoryDistributionMembers(): Promise<
Array<StoryDistributionMemberType>
> {
return channels._getAllStoryDistributionMembers();
}
async function _deleteAllStoryDistributions(): Promise<void> {
await channels._deleteAllStoryDistributions();
}
async function createNewStoryDistribution(
distribution: StoryDistributionWithMembersType
): Promise<void> {
strictAssert(
distribution.name,
'Distribution list does not have a valid name'
);
await channels.createNewStoryDistribution(distribution);
}
async function getAllStoryDistributionsWithMembers(): Promise<
Array<StoryDistributionWithMembersType>
> {
return channels.getAllStoryDistributionsWithMembers();
}
2022-03-04 21:14:52 +00:00
async function getStoryDistributionWithMembers(
id: string
): Promise<StoryDistributionWithMembersType | undefined> {
return channels.getStoryDistributionWithMembers(id);
}
async function modifyStoryDistribution(
distribution: StoryDistributionType
): Promise<void> {
if (distribution.deletedAtTimestamp) {
strictAssert(
!distribution.name,
'Attempt to delete distribution list but still has a name'
);
} else {
strictAssert(
distribution.name,
'Cannot clear distribution list name without deletedAtTimestamp set'
);
}
await channels.modifyStoryDistribution(distribution);
}
async function modifyStoryDistributionMembers(
id: string,
options: {
toAdd: Array<UUIDStringType>;
toRemove: Array<UUIDStringType>;
}
): Promise<void> {
await channels.modifyStoryDistributionMembers(id, options);
}
2022-07-01 00:52:03 +00:00
async function modifyStoryDistributionWithMembers(
distribution: StoryDistributionType,
options: {
toAdd: Array<UUIDStringType>;
toRemove: Array<UUIDStringType>;
}
): Promise<void> {
if (distribution.deletedAtTimestamp) {
strictAssert(
!distribution.name,
'Attempt to delete distribution list but still has a name'
);
} else {
strictAssert(
distribution.name,
'Cannot clear distribution list name without deletedAtTimestamp set'
);
}
2022-07-01 00:52:03 +00:00
await channels.modifyStoryDistributionWithMembers(distribution, options);
}
async function deleteStoryDistribution(id: UUIDStringType): Promise<void> {
await channels.deleteStoryDistribution(id);
}
// Story Reads
async function _getAllStoryReads(): Promise<Array<StoryReadType>> {
return channels._getAllStoryReads();
}
async function _deleteAllStoryReads(): Promise<void> {
await channels._deleteAllStoryReads();
}
async function addNewStoryRead(read: StoryReadType): Promise<void> {
return channels.addNewStoryRead(read);
}
async function getLastStoryReadsForAuthor(options: {
authorId: UUIDStringType;
conversationId?: UUIDStringType;
limit?: number;
}): Promise<Array<StoryReadType>> {
return channels.getLastStoryReadsForAuthor(options);
}
2022-03-29 01:10:08 +00:00
async function countStoryReadsByConversation(
conversationId: string
): Promise<number> {
return channels.countStoryReadsByConversation(conversationId);
}
2018-10-18 01:01:21 +00:00
// Other
2022-07-28 16:35:29 +00:00
async function removeAll(): Promise<void> {
await channels.removeAll();
}
2022-07-28 16:35:29 +00:00
async function removeAllConfiguration(
type?: RemoveAllConfiguration
): Promise<void> {
await channels.removeAllConfiguration(type);
2018-10-18 01:01:21 +00:00
}
2022-07-28 16:35:29 +00:00
async function cleanupOrphanedAttachments(): Promise<void> {
2018-08-08 17:00:33 +00:00
await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
}
2022-07-28 16:35:29 +00:00
async function ensureFilePermissions(): Promise<void> {
await callChannel(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([
callChannel(ERASE_SQL_KEY),
callChannel(ERASE_ATTACHMENTS_KEY),
callChannel(ERASE_STICKERS_KEY),
callChannel(ERASE_TEMP_KEY),
2019-08-07 00:40:25 +00:00
callChannel(ERASE_DRAFTS_KEY),
]);
}
2022-07-28 16:35:29 +00:00
async function callChannel(name: string): Promise<void> {
return createTaskWithTimeout(
() =>
new Promise<void>((resolve, reject) => {
2021-10-07 18:16:51 +00:00
ipc.send(name);
ipc.once(`${name}-done`, (_, error) => {
if (error) {
reject(error);
return;
}
resolve();
});
}),
`callChannel call to ${name}`
)();
}
async function getMessagesNeedingUpgrade(
limit: number,
{ maxVersion = CURRENT_SCHEMA_VERSION }: { maxVersion: number }
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
const messages = await channels.getMessagesNeedingUpgrade(limit, {
maxVersion,
});
return messages;
}
async function getMessagesWithVisualMediaAttachments(
conversationId: string,
{ limit }: { limit: number }
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
return channels.getMessagesWithVisualMediaAttachments(conversationId, {
limit,
});
}
async function getMessagesWithFileAttachments(
conversationId: string,
{ limit }: { limit: number }
2022-07-28 16:35:29 +00:00
): Promise<Array<MessageType>> {
return channels.getMessagesWithFileAttachments(conversationId, {
limit,
});
}
2021-04-29 23:02:27 +00:00
function getMessageServerGuidsForSpam(
conversationId: string
): Promise<Array<string>> {
return channels.getMessageServerGuidsForSpam(conversationId);
}
2021-04-29 23:02:27 +00:00
function getJobsInQueue(queueType: string): Promise<Array<StoredJob>> {
return channels.getJobsInQueue(queueType);
}
function insertJob(job: Readonly<StoredJob>): Promise<void> {
return channels.insertJob(job);
}
function deleteJob(id: string): Promise<void> {
return channels.deleteJob(id);
}
2021-05-28 16:15:17 +00:00
2021-08-20 16:06:15 +00:00
function processGroupCallRingRequest(
ringId: bigint
): Promise<ProcessGroupCallRingRequestResult> {
return channels.processGroupCallRingRequest(ringId);
}
function processGroupCallRingCancelation(ringId: bigint): Promise<void> {
return channels.processGroupCallRingCancelation(ringId);
}
async function cleanExpiredGroupCallRings(): Promise<void> {
await channels.cleanExpiredGroupCallRings();
}
2021-05-28 16:15:17 +00:00
async function updateAllConversationColors(
conversationColor?: ConversationColorType,
customColorData?: {
id: string;
value: CustomColorType;
}
): Promise<void> {
return channels.updateAllConversationColors(
conversationColor,
customColorData
);
}
2022-07-08 20:46:25 +00:00
async function removeAllProfileKeyCredentials(): Promise<void> {
return channels.removeAllProfileKeyCredentials();
}
2021-09-15 18:45:22 +00:00
function getMaxMessageCounter(): Promise<number | undefined> {
return channels.getMaxMessageCounter();
}
function getStatisticsForLogging(): Promise<Record<string, string>> {
return channels.getStatisticsForLogging();
}