diff --git a/src/@types/keytar.d.ts b/src/@types/keytar.d.ts new file mode 100644 index 0000000..62882c9 --- /dev/null +++ b/src/@types/keytar.d.ts @@ -0,0 +1,54 @@ +// Based on https://github.com/atom/node-keytar/blob/master/keytar.d.ts because keytar is a hak-dependency and not a normal one +// Definitions by: Milan Burda , Brendan Forster , Hari Juturu +// Adapted from DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/keytar/index.d.ts + +declare module "keytar" { + /** + * Get the stored password for the service and account. + * + * @param service The string service name. + * @param account The string account name. + * + * @returns A promise for the password string. + */ + export function getPassword(service: string, account: string): Promise; + + /** + * Add the password for the service and account to the keychain. + * + * @param service The string service name. + * @param account The string account name. + * @param password The string password. + * + * @returns A promise for the set password completion. + */ + export function setPassword(service: string, account: string, password: string): Promise; + + /** + * Delete the stored password for the service and account. + * + * @param service The string service name. + * @param account The string account name. + * + * @returns A promise for the deletion status. True on success. + */ + export function deletePassword(service: string, account: string): Promise; + + /** + * Find a password for the service in the keychain. + * + * @param service The string service name. + * + * @returns A promise for the password string. + */ + export function findPassword(service: string): Promise; + + /** + * Find all accounts and passwords for `service` in the keychain. + * + * @param service The string service name. + * + * @returns A promise for the array of found credentials. + */ + export function findCredentials(service: string): Promise>; +} diff --git a/src/@types/matrix-seshat.d.ts b/src/@types/matrix-seshat.d.ts new file mode 100644 index 0000000..133a37e --- /dev/null +++ b/src/@types/matrix-seshat.d.ts @@ -0,0 +1,145 @@ +/* +Copyright 2022 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +declare module "matrix-seshat" { + interface IConfig { + language?: string; + passphrase?: string; + } + + /* eslint-disable camelcase */ + interface IMatrixEvent { + event_id: string; + sender: string; + room_id: string; + origin_server_ts: number; + content: Record; + } + + interface IMatrixProfile { + displayname?: string; + avatar_url?: string; + } + + interface ISearchArgs { + searchTerm: number; + limit: number; + before_limit: number; + after_limit: number; + order_by_recency: boolean; + next_batch?: string; + } + + interface ISearchContext { + events_before: IMatrixEvent[]; + events_after: IMatrixEvent[]; + profile_info: { [userId: string]: IMatrixProfile }; + } + + interface ISearchResult { + next_batch: string; + count: number; + results: Array<{ + rank: number; + result: IMatrixEvent; + context: ISearchContext; + }>; + } + /* eslint-enable camelcase */ + + interface ICheckpoint { + roomId: string; + token: string; + fullCrawl: boolean; + direction: "b" | "f"; + } + + interface IDatabaseStats { + size: number; + eventCount: number; + roomCount: number; + } + + interface ILoadArgs { + roomId: string; + limit: number; + fromEvent: string; + direction: "b" | "f"; + } + + interface ILoadResult { + event: IMatrixEvent; + matrixProfile: IMatrixProfile; + } + + export class Seshat { + constructor(path: string, config?: IConfig); + public addEvent(matrixEvent: IMatrixEvent, profile?: IMatrixProfile): void; + public deleteEvent(eventId: string): Promise; + public commit(force?: boolean): Promise; + public commitSync(wait?: boolean, force?: boolean): number; + public reload(): void; + public search(args: ISearchArgs): Promise; + public searchSync( + term: string, + limit?: number, + beforeLimit?: number, + afterLimit?: number, + orderByRecency?: boolean, + ): ISearchResult; + public addHistoricEventsSync( + events: IMatrixEvent[], + newCheckpoint?: ICheckpoint, + oldCheckpoint?: ICheckpoint, + ): boolean; + public addHistoricEvents( + events: IMatrixEvent[], + newCheckpoint?: ICheckpoint, + oldCheckpoint?: ICheckpoint, + ): Promise; + public addCrawlerCheckpoint(checkpoint: ICheckpoint): Promise; + public removeCrawlerCheckpoint(checkpoint: ICheckpoint): Promise; + public loadCheckpoints(): Promise; + public getSize(): Promise; + public getStats(): Promise; + public delete(): Promise; + public shutdown(): Promise; + public changePassphrase(newPassphrase: string): Promise; + public isEmpty(): Promise; + public isRoomIndexed(roomId: string): Promise; + public getUserVersion(): Promise; + public setUserVersion(version: number): Promise; + public loadFileEvents(args: ILoadArgs): Promise; + } + + interface IRecoveryInfo { + totalEvents: number; + reindexedEvents: number; + done: number; + } + + export class SeshatRecovery { + constructor(path: string, config?: IConfig); + public info(): IRecoveryInfo; + public getUserVersion(): Promise; + public shutdown(): Promise; + public reindex(): Promise; + } + + export class ReindexError extends Error { + constructor(message?: string); + } +} diff --git a/src/electron-main.ts b/src/electron-main.ts index 68d2506..57b99df 100644 --- a/src/electron-main.ts +++ b/src/electron-main.ts @@ -39,18 +39,26 @@ import crypto from "crypto"; import { URL } from "url"; import minimist from "minimist"; +import type * as Keytar from "keytar"; // Hak dependency type +import type { + Seshat as SeshatType, + SeshatRecovery as SeshatRecoveryType, + ReindexError as ReindexErrorType, +} from "matrix-seshat"; // Hak dependency type import * as tray from "./tray"; import { buildMenuTemplate } from './vectormenu'; import webContentsHandler from './webcontents-handler'; import * as updater from './updater'; import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol'; import { _t, AppLocalization } from './language-helper'; +import Input = Electron.Input; +import IpcMainEvent = Electron.IpcMainEvent; const argv = minimist(process.argv, { alias: { help: "h" }, }); -let keytar; +let keytar: typeof Keytar; try { // eslint-disable-next-line @typescript-eslint/no-var-requires keytar = require('keytar'); @@ -63,9 +71,9 @@ try { } let seshatSupported = false; -let Seshat; -let SeshatRecovery; -let ReindexError; +let Seshat: typeof SeshatType; +let SeshatRecovery: typeof SeshatRecoveryType; +let ReindexError: typeof ReindexErrorType; try { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -84,13 +92,18 @@ try { // Things we need throughout the file but need to be created // async to are initialised in setupGlobals() -let asarPath; -let resPath; -let vectorConfig; -let iconPath; -let trayConfig; -let launcher; -let appLocalization; +let asarPath: string; +let resPath: string; +let iconPath: string; + +let vectorConfig: Record; +let trayConfig: { + // eslint-disable-next-line camelcase + icon_path: string; + brand: string; +}; +let launcher: AutoLaunch; +let appLocalization: AppLocalization; if (argv["help"]) { console.log("Options:"); @@ -108,12 +121,12 @@ if (argv["help"]) { // Electron creates the user data directory (with just an empty 'Dictionaries' directory...) // as soon as the app path is set, so pick a random path in it that must exist if it's a // real user data directory. -function isRealUserDataDir(d) { +function isRealUserDataDir(d: string): boolean { return fs.existsSync(path.join(d, 'IndexedDB')); } // check if we are passed a profile in the SSO callback url -let userDataPath; +let userDataPath: string; const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]); if (userDataPathInProtocol) { @@ -143,7 +156,7 @@ if (userDataPathInProtocol) { } app.setPath('userData', userDataPath); -async function tryPaths(name, root, rawPaths) { +async function tryPaths(name: string, root: string, rawPaths: string[]): Promise { // Make everything relative to root const paths = rawPaths.map(p => path.join(root, p)); @@ -162,7 +175,7 @@ async function tryPaths(name, root, rawPaths) { } // Find the webapp resources and set up things that require them -async function setupGlobals() { +async function setupGlobals(): Promise { // find the webapp asar. asarPath = await tryPaths("webapp", __dirname, [ // If run from the source checkout, this will be in the directory above @@ -245,9 +258,9 @@ async function setupGlobals() { }); } -async function moveAutoLauncher() { +async function moveAutoLauncher(): Promise { // Look for an auto-launcher under 'Riot' and if we find one, port it's - // enabled/disbaledp-ness over to the new 'Element' launcher + // enabled/disabled-ness over to the new 'Element' launcher if (!vectorConfig.brand || vectorConfig.brand === 'Element') { const oldLauncher = new AutoLaunch({ name: 'Riot', @@ -274,18 +287,18 @@ const store = new Store<{ disableHardwareAcceleration?: boolean; }>({ name: "electron-config" }); -let eventIndex = null; +let eventIndex: SeshatType = null; -let mainWindow = null; +let mainWindow: BrowserWindow = null; global.appQuitting = false; -const exitShortcuts = [ +const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [ (input, platform) => platform !== 'darwin' && input.alt && input.key.toUpperCase() === 'F4', (input, platform) => platform !== 'darwin' && input.control && input.key.toUpperCase() === 'Q', (input, platform) => platform === 'darwin' && input.meta && input.key.toUpperCase() === 'Q', ]; -const warnBeforeExit = (event, input) => { +const warnBeforeExit = (event: Event, input: Input): void => { const shouldWarnBeforeExit = store.get('warnBeforeExit', true); const exitShortcutPressed = input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform)); @@ -305,14 +318,14 @@ const warnBeforeExit = (event, input) => { } }; -const deleteContents = async (p) => { +const deleteContents = async (p: string): Promise => { for (const entry of await afs.readdir(p)) { const curPath = path.join(p, entry); await afs.unlink(curPath); } }; -async function randomArray(size) { +async function randomArray(size: number): Promise { return new Promise((resolve, reject) => { crypto.randomBytes(size, (err, buf) => { if (err) { @@ -330,12 +343,12 @@ async function randomArray(size) { // no other way to catch this error). // Assuming we generally run from the console when developing, // this is far preferable. -process.on('uncaughtException', function(error) { +process.on('uncaughtException', function(error: Error): void { console.log('Unhandled exception', error); }); let focusHandlerAttached = false; -ipcMain.on('setBadgeCount', function(ev, count) { +ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void { if (process.platform !== 'win32') { // only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron // has some Windows support too, and in some Windows environments this leads to two badges rendering atop @@ -347,7 +360,7 @@ ipcMain.on('setBadgeCount', function(ev, count) { } }); -ipcMain.on('loudNotification', function() { +ipcMain.on('loudNotification', function(): void { if (process.platform === 'win32' && mainWindow && !mainWindow.isFocused() && !focusHandlerAttached) { mainWindow.flashFrame(true); mainWindow.once('focus', () => { @@ -358,8 +371,8 @@ ipcMain.on('loudNotification', function() { } }); -let powerSaveBlockerId = null; -ipcMain.on('app_onAction', function(ev, payload) { +let powerSaveBlockerId: number = null; +ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) { switch (payload.action) { case 'call_state': if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) { @@ -376,11 +389,11 @@ ipcMain.on('app_onAction', function(ev, payload) { } }); -ipcMain.on('ipcCall', async function(ev, payload) { +ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) { if (!mainWindow) return; const args = payload.args || []; - let ret; + let ret: any; switch (payload.name) { case 'getUpdateFeedUrl': @@ -542,7 +555,7 @@ ipcMain.on('ipcCall', async function(ev, payload) { }); const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE"; -async function getOrCreatePassphrase(key) { +async function getOrCreatePassphrase(key: string): Promise { if (keytar) { try { const storedPassphrase = await keytar.getPassword("element.io", key); @@ -561,7 +574,7 @@ async function getOrCreatePassphrase(key) { } } -ipcMain.on('seshat', async function(ev, payload) { +ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise { if (!mainWindow) return; const sendError = (id, e) => { @@ -576,7 +589,7 @@ ipcMain.on('seshat', async function(ev, payload) { }; const args = payload.args || []; - let ret; + let ret: any; switch (payload.name) { case 'supportsEventIndexing': @@ -913,7 +926,7 @@ app.on('ready', async () => { target[target.length - 1] = 'index.html'; } - let baseDir; + let baseDir: string; if (target[1] === 'webapp') { baseDir = asarPath; } else { @@ -1048,7 +1061,7 @@ app.on('activate', () => { mainWindow.show(); }); -function beforeQuit() { +function beforeQuit(): void { global.appQuitting = true; if (mainWindow) { mainWindow.webContents.send('before-quit'); diff --git a/src/protocol.ts b/src/protocol.ts index 06b78c0..7e3bd8b 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -80,7 +80,7 @@ export function recordSSOSession(sessionID: string): void { writeStore(store); } -export function getProfileFromDeeplink(args): string | undefined { +export function getProfileFromDeeplink(args: string[]): string | undefined { // check if we are passed a profile in the SSO callback url const deeplinkUrl = args.find(arg => arg.startsWith(PROTOCOL + '//')); if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {