signal-desktop/ts/CI.ts

196 lines
5 KiB
TypeScript
Raw Normal View History

2021-08-11 19:29:07 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2024-09-12 23:48:27 +00:00
import { format } from 'node:util';
2021-08-11 19:29:07 +00:00
import { ipcRenderer } from 'electron';
2023-08-15 23:24:19 +00:00
import type { IPCResponse as ChallengeResponseType } from './challenge';
import type { MessageAttributesType } from './model-types.d';
import * as log from './logging/log';
2021-08-11 19:29:07 +00:00
import { explodePromise } from './util/explodePromise';
2024-07-22 18:16:33 +00:00
import { AccessType, ipcInvoke } from './sql/channels';
import { backupsService } from './services/backups';
2021-11-08 21:43:37 +00:00
import { SECOND } from './util/durations';
2023-11-02 19:42:31 +00:00
import { isSignalRoute } from './util/signalRoutes';
import { strictAssert } from './util/assert';
2021-08-11 19:29:07 +00:00
type ResolveType = (data: unknown) => void;
2023-01-13 00:24:59 +00:00
export type CIType = {
deviceName: string;
2023-08-15 23:24:19 +00:00
getConversationId: (address: string | null) => string | null;
getMessagesBySentAt(
sentAt: number
): Promise<ReadonlyArray<MessageAttributesType>>;
getPendingEventCount: (event: string) => number;
2023-01-13 00:24:59 +00:00
handleEvent: (event: string, data: unknown) => unknown;
setProvisioningURL: (url: string) => unknown;
solveChallenge: (response: ChallengeResponseType) => unknown;
waitForEvent: (
event: string,
options: {
timeout?: number;
ignorePastEvents?: boolean;
}
) => unknown;
2023-11-02 19:42:31 +00:00
openSignalRoute(url: string): Promise<void>;
uploadBackup(): Promise<void>;
unlink: () => void;
2024-09-12 23:48:27 +00:00
print: (...args: ReadonlyArray<unknown>) => void;
2023-01-13 00:24:59 +00:00
};
2024-03-15 14:20:33 +00:00
export type GetCIOptionsType = Readonly<{
deviceName: string;
}>;
export function getCI({ deviceName }: GetCIOptionsType): CIType {
2023-01-13 00:24:59 +00:00
const eventListeners = new Map<string, Array<ResolveType>>();
const completedEvents = new Map<string, Array<unknown>>();
ipcRenderer.on('ci:event', (_, event, data) => {
handleEvent(event, data);
});
function waitForEvent(
event: string,
options: {
timeout?: number;
ignorePastEvents?: boolean;
} = {}
) {
const timeout = options?.timeout ?? 60 * SECOND;
2021-08-11 19:29:07 +00:00
if (!options?.ignorePastEvents) {
const pendingCompleted = completedEvents.get(event) || [];
if (pendingCompleted.length) {
const pending = pendingCompleted.shift();
log.info(`CI: resolving pending result for ${event}`, pending);
2021-08-11 19:29:07 +00:00
if (pendingCompleted.length === 0) {
completedEvents.delete(event);
}
return pending;
}
2021-08-11 19:29:07 +00:00
}
log.info(`CI: waiting for event ${event}`);
2021-11-08 21:43:37 +00:00
const { resolve, reject, promise } = explodePromise();
const timer = setTimeout(() => {
reject(new Error('Timed out'));
}, timeout);
2021-08-11 19:29:07 +00:00
2023-01-13 00:24:59 +00:00
let list = eventListeners.get(event);
2021-08-11 19:29:07 +00:00
if (!list) {
list = [];
2023-01-13 00:24:59 +00:00
eventListeners.set(event, list);
2021-08-11 19:29:07 +00:00
}
2021-11-08 21:43:37 +00:00
list.push((value: unknown) => {
clearTimeout(timer);
resolve(value);
});
2021-08-11 19:29:07 +00:00
return promise;
}
function getPendingEventCount(event: string): number {
const completed = completedEvents.get(event) || [];
return completed.length;
}
2023-01-13 00:24:59 +00:00
function setProvisioningURL(url: string): void {
handleEvent('provisioning-url', url);
2021-08-11 19:29:07 +00:00
}
2023-01-13 00:24:59 +00:00
function handleEvent(event: string, data: unknown): void {
const list = eventListeners.get(event) || [];
2021-08-11 19:29:07 +00:00
const resolve = list.shift();
if (resolve) {
if (list.length === 0) {
2023-01-13 00:24:59 +00:00
eventListeners.delete(event);
2021-08-11 19:29:07 +00:00
}
log.info(`CI: got event ${event} with data`, data);
2021-08-11 19:29:07 +00:00
resolve(data);
return;
}
log.info(`CI: postponing event ${event}`);
2021-08-11 19:29:07 +00:00
2023-01-13 00:24:59 +00:00
let resultList = completedEvents.get(event);
2021-08-11 19:29:07 +00:00
if (!resultList) {
resultList = [];
2023-01-13 00:24:59 +00:00
completedEvents.set(event, resultList);
2021-08-11 19:29:07 +00:00
}
resultList.push(data);
}
2023-01-13 00:24:59 +00:00
function solveChallenge(response: ChallengeResponseType): void {
window.Signal.challengeHandler?.onResponse(response);
}
2023-01-13 00:24:59 +00:00
2023-08-15 23:24:19 +00:00
async function getMessagesBySentAt(sentAt: number) {
const messages = await ipcInvoke<ReadonlyArray<MessageAttributesType>>(
2024-07-22 18:16:33 +00:00
AccessType.Read,
2023-08-15 23:24:19 +00:00
'getMessagesBySentAt',
[sentAt]
);
return messages.map(
m =>
window.MessageCache.__DEPRECATED$register(
m.id,
m,
'CI.getMessagesBySentAt'
).attributes
2023-08-15 23:24:19 +00:00
);
}
function getConversationId(address: string | null): string | null {
return window.ConversationController.getConversationId(address);
}
2023-11-02 19:42:31 +00:00
async function openSignalRoute(url: string) {
strictAssert(
isSignalRoute(url),
`openSignalRoute: not a valid signal route ${url}`
);
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.hidden = true;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
async function uploadBackup() {
await backupsService.upload();
2024-09-03 17:18:15 +00:00
}
function unlink() {
window.Whisper.events.trigger('unlinkAndDisconnect');
}
2024-09-12 23:48:27 +00:00
function print(...args: ReadonlyArray<unknown>) {
handleEvent('print', format(...args));
}
2023-01-13 00:24:59 +00:00
return {
deviceName,
2023-08-15 23:24:19 +00:00
getConversationId,
getMessagesBySentAt,
2023-01-13 00:24:59 +00:00
handleEvent,
setProvisioningURL,
solveChallenge,
waitForEvent,
2023-11-02 19:42:31 +00:00
openSignalRoute,
uploadBackup,
unlink,
getPendingEventCount,
2024-09-12 23:48:27 +00:00
print,
2023-01-13 00:24:59 +00:00
};
2021-08-11 19:29:07 +00:00
}