2019-12-06 19:17:34 +01:00
/ *
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2017 , 2019 Michael Telatynski < 7t3chguy @ gmail.com >
2021-01-13 16:21:00 +01:00
Copyright 2018 - 2021 New Vector Ltd
2019-12-06 19:17:34 +01:00
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 .
* /
2021-07-27 12:47:44 +02:00
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
import "./squirrelhooks" ;
2022-02-11 11:38:19 +01:00
import {
app ,
ipcMain ,
powerSaveBlocker ,
BrowserWindow ,
Menu ,
autoUpdater ,
protocol ,
dialog ,
desktopCapturer ,
} from "electron" ;
2021-06-25 15:35:58 +02:00
import AutoLaunch from "auto-launch" ;
import path from "path" ;
import windowStateKeeper from 'electron-window-state' ;
import Store from 'electron-store' ;
import fs , { promises as afs } from "fs" ;
import crypto from "crypto" ;
import { URL } from "url" ;
2021-07-01 10:22:57 +02:00
import minimist from "minimist" ;
2021-06-25 15:35:58 +02:00
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' ;
2019-12-06 19:17:34 +01:00
2021-07-01 10:22:57 +02:00
const argv = minimist ( process . argv , {
alias : { help : "h" } ,
} ) ;
2020-05-28 21:07:39 +02:00
let keytar ;
try {
2021-07-01 10:22:57 +02:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2020-05-28 21:07:39 +02:00
keytar = require ( 'keytar' ) ;
} catch ( e ) {
if ( e . code === "MODULE_NOT_FOUND" ) {
console . log ( "Keytar isn't installed; secure key storage is disabled." ) ;
} else {
console . warn ( "Keytar unexpected error:" , e ) ;
}
}
2020-03-11 11:27:00 +01:00
let seshatSupported = false ;
let Seshat ;
let SeshatRecovery ;
let ReindexError ;
2019-12-06 19:17:34 +01:00
try {
2021-07-01 10:22:57 +02:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2020-03-11 12:05:34 +01:00
const seshatModule = require ( 'matrix-seshat' ) ;
2020-03-11 11:27:00 +01:00
Seshat = seshatModule . Seshat ;
SeshatRecovery = seshatModule . SeshatRecovery ;
ReindexError = seshatModule . ReindexError ;
seshatSupported = true ;
2019-12-06 19:17:34 +01:00
} catch ( e ) {
2020-02-03 17:19:16 +01:00
if ( e . code === "MODULE_NOT_FOUND" ) {
console . log ( "Seshat isn't installed, event indexing is disabled." ) ;
} else {
console . warn ( "Seshat unexpected error:" , e ) ;
}
2019-12-06 19:17:34 +01:00
}
2019-12-10 18:40:17 +01:00
// 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 ;
2021-04-26 14:58:29 +02:00
let appLocalization ;
2019-12-10 18:40:17 +01:00
2019-12-06 19:17:34 +01:00
if ( argv [ "help" ] ) {
console . log ( "Options:" ) ;
console . log ( " --profile-dir {path}: Path to where to store the profile." ) ;
console . log ( " --profile {name}: Name of alternate profile to use, allows for running multiple accounts." ) ;
console . log ( " --devtools: Install and use react-devtools and react-perf." ) ;
console . log ( " --no-update: Disable automatic updating." ) ;
console . log ( " --hidden: Start the application hidden in the system tray." ) ;
console . log ( " --help: Displays this help message." ) ;
console . log ( "And more such as --proxy, see:" +
2020-05-14 12:27:26 +02:00
"https://electronjs.org/docs/api/command-line-switches" ) ;
2019-12-06 19:17:34 +01:00
app . exit ( ) ;
}
2020-07-15 12:41:30 +02:00
// 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 ) {
return fs . existsSync ( path . join ( d , 'IndexedDB' ) ) ;
}
2020-04-14 14:29:47 +02:00
// check if we are passed a profile in the SSO callback url
2020-07-07 19:57:29 +02:00
let userDataPath ;
2020-04-14 14:29:47 +02:00
const userDataPathInProtocol = getProfileFromDeeplink ( argv [ "_" ] ) ;
if ( userDataPathInProtocol ) {
2020-07-07 19:57:29 +02:00
userDataPath = userDataPathInProtocol ;
2020-04-14 14:29:47 +02:00
} else if ( argv [ 'profile-dir' ] ) {
2020-07-07 19:57:29 +02:00
userDataPath = argv [ 'profile-dir' ] ;
2020-07-02 14:30:11 +02:00
} else {
2020-07-21 18:57:54 +02:00
let newUserDataPath = app . getPath ( 'userData' ) ;
2020-07-02 14:30:11 +02:00
if ( argv [ 'profile' ] ) {
newUserDataPath += '-' + argv [ 'profile' ] ;
}
const newUserDataPathExists = isRealUserDataDir ( newUserDataPath ) ;
2020-07-21 18:57:54 +02:00
let oldUserDataPath = path . join ( app . getPath ( 'appData' ) , app . getName ( ) . replace ( 'Element' , 'Riot' ) ) ;
if ( argv [ 'profile' ] ) {
oldUserDataPath += '-' + argv [ 'profile' ] ;
}
2020-07-02 14:30:11 +02:00
const oldUserDataPathExists = isRealUserDataDir ( oldUserDataPath ) ;
console . log ( newUserDataPath + " exists: " + ( newUserDataPathExists ? 'yes' : 'no' ) ) ;
console . log ( oldUserDataPath + " exists: " + ( oldUserDataPathExists ? 'yes' : 'no' ) ) ;
if ( ! newUserDataPathExists && oldUserDataPathExists ) {
console . log ( "Using legacy user data path: " + oldUserDataPath ) ;
2020-07-07 19:57:29 +02:00
userDataPath = oldUserDataPath ;
} else {
userDataPath = newUserDataPath ;
2020-07-02 14:30:11 +02:00
}
2019-12-06 19:17:34 +01:00
}
2020-07-07 19:57:29 +02:00
app . setPath ( 'userData' , userDataPath ) ;
2019-12-06 19:17:34 +01:00
2020-04-16 14:08:16 +02:00
async function tryPaths ( name , root , rawPaths ) {
// Make everything relative to root
const paths = rawPaths . map ( p = > path . join ( root , p ) ) ;
2019-12-10 18:40:17 +01:00
for ( const p of paths ) {
try {
await afs . stat ( p ) ;
return p + '/' ;
} catch ( e ) {
}
}
2020-04-16 14:08:16 +02:00
console . log ( ` Couldn't find ${ name } files in any of: ` ) ;
2019-12-10 18:40:17 +01:00
for ( const p of paths ) {
console . log ( "\t" + path . resolve ( p ) ) ;
}
2020-04-16 14:08:16 +02:00
throw new Error ( ` Failed to find ${ name } files ` ) ;
2019-12-06 19:17:34 +01:00
}
2019-12-10 18:40:17 +01:00
// Find the webapp resources and set up things that require them
async function setupGlobals() {
// find the webapp asar.
2020-04-16 14:08:16 +02:00
asarPath = await tryPaths ( "webapp" , __dirname , [
2019-12-10 18:40:17 +01:00
// If run from the source checkout, this will be in the directory above
'../webapp.asar' ,
// but if run from a packaged application, electron-main.js will be in
// a different asar file so it will be two levels above
'../../webapp.asar' ,
2020-04-16 14:08:16 +02:00
// also try without the 'asar' suffix to allow symlinking in a directory
2019-12-10 18:40:17 +01:00
'../webapp' ,
2020-04-16 14:08:16 +02:00
// from a packaged application
'../../webapp' ,
2019-12-10 18:40:17 +01:00
] ) ;
2020-04-16 14:08:16 +02:00
2019-12-10 18:40:17 +01:00
// we assume the resources path is in the same place as the asar
2020-04-16 14:08:16 +02:00
resPath = await tryPaths ( "res" , path . dirname ( asarPath ) , [
// If run from the source checkout
'res' ,
// if run from packaged application
'' ,
] ) ;
2019-12-10 18:40:17 +01:00
try {
2021-07-01 10:22:57 +02:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2019-12-10 18:40:17 +01:00
vectorConfig = require ( asarPath + 'config.json' ) ;
} catch ( e ) {
// it would be nice to check the error code here and bail if the config
2020-11-14 22:54:07 +01:00
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
2019-12-10 18:40:17 +01:00
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
vectorConfig = { } ;
2019-12-06 19:17:34 +01:00
}
2019-12-10 18:40:17 +01:00
try {
// Load local config and use it to override values from the one baked with the build
2021-07-01 10:24:02 +02:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2019-12-10 18:40:17 +01:00
const localConfig = require ( path . join ( app . getPath ( 'userData' ) , 'config.json' ) ) ;
// If the local config has a homeserver defined, don't use the homeserver from the build
// config. This is to avoid a problem where Riot thinks there are multiple homeservers
// defined, and panics as a result.
const homeserverProps = [ 'default_is_url' , 'default_hs_url' , 'default_server_name' , 'default_server_config' ] ;
if ( Object . keys ( localConfig ) . find ( k = > homeserverProps . includes ( k ) ) ) {
// Rip out all the homeserver options from the vector config
vectorConfig = Object . keys ( vectorConfig )
. filter ( k = > ! homeserverProps . includes ( k ) )
. reduce ( ( obj , key ) = > { obj [ key ] = vectorConfig [ key ] ; return obj ; } , { } ) ;
}
vectorConfig = Object . assign ( vectorConfig , localConfig ) ;
} catch ( e ) {
2021-06-04 06:39:29 +02:00
if ( e instanceof SyntaxError ) {
dialog . showMessageBox ( {
type : "error" ,
2021-06-05 00:17:58 +02:00
title : ` Your ${ vectorConfig . brand || 'Element' } is misconfigured ` ,
message : ` Your custom ${ vectorConfig . brand || 'Element' } configuration contains invalid JSON. ` +
` Please correct the problem and reopen ${ vectorConfig . brand || 'Element' } . ` ,
2021-06-04 06:39:29 +02:00
detail : e.message || "" ,
} ) ;
}
2019-12-10 18:40:17 +01:00
// Could not load local config, this is expected in most cases.
}
// The tray icon
// It's important to call `path.join` so we don't end up with the packaged asar in the final path.
2020-07-15 14:50:00 +02:00
const iconFile = ` element. ${ process . platform === 'win32' ? 'ico' : 'png' } ` ;
2019-12-10 18:40:17 +01:00
iconPath = path . join ( resPath , "img" , iconFile ) ;
trayConfig = {
icon_path : iconPath ,
2020-07-01 16:40:23 +02:00
brand : vectorConfig.brand || 'Element' ,
2019-12-10 18:40:17 +01:00
} ;
// launcher
launcher = new AutoLaunch ( {
2020-07-02 14:30:11 +02:00
name : vectorConfig.brand || 'Element' ,
2019-12-10 18:40:17 +01:00
isHidden : true ,
mac : {
useLaunchAgent : true ,
} ,
} ) ;
2019-12-06 19:17:34 +01:00
}
2020-07-02 14:30:11 +02:00
async function moveAutoLauncher() {
2020-07-02 15:42:45 +02:00
// Look for an auto-launcher under 'Riot' and if we find one, port it's
2020-07-02 14:30:11 +02:00
// enabled/disbaledp-ness over to the new 'Element' launcher
if ( ! vectorConfig . brand || vectorConfig . brand === 'Element' ) {
const oldLauncher = new AutoLaunch ( {
name : 'Riot' ,
isHidden : true ,
mac : {
useLaunchAgent : true ,
} ,
} ) ;
const wasEnabled = await oldLauncher . isEnabled ( ) ;
if ( wasEnabled ) {
await oldLauncher . disable ( ) ;
await launcher . enable ( ) ;
}
}
}
2019-12-06 19:17:34 +01:00
const eventStorePath = path . join ( app . getPath ( 'userData' ) , 'EventStore' ) ;
2021-06-25 15:35:58 +02:00
const store = new Store < {
warnBeforeExit? : boolean ;
minimizeToTray? : boolean ;
spellCheckerEnabled? : boolean ;
autoHideMenuBar? : boolean ;
locale? : string | string [ ] ;
} > ( { name : "electron-config" } ) ;
2019-12-06 19:17:34 +01:00
let eventIndex = null ;
let mainWindow = null ;
global . appQuitting = false ;
2021-03-31 09:58:24 +02:00
const exitShortcuts = [
2021-04-15 17:50:35 +02:00
( 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' ,
2021-03-31 09:58:24 +02:00
] ;
const warnBeforeExit = ( event , input ) = > {
const shouldWarnBeforeExit = store . get ( 'warnBeforeExit' , true ) ;
2021-06-21 15:17:28 +02:00
const exitShortcutPressed =
input . type === 'keyDown' && exitShortcuts . some ( shortcutFn = > shortcutFn ( input , process . platform ) ) ;
2021-03-31 09:58:24 +02:00
2021-03-31 18:18:39 +02:00
if ( shouldWarnBeforeExit && exitShortcutPressed ) {
2021-03-29 13:10:27 +02:00
const shouldCancelCloseRequest = dialog . showMessageBoxSync ( mainWindow , {
type : "question" ,
2021-04-26 15:50:18 +02:00
buttons : [ _t ( "Cancel" ) , _t ( "Close Element" ) ] ,
message : _t ( "Are you sure you want to quit?" ) ,
2021-03-29 13:10:27 +02:00
defaultId : 1 ,
cancelId : 0 ,
} ) === 0 ;
if ( shouldCancelCloseRequest ) {
event . preventDefault ( ) ;
}
}
} ;
2020-06-23 16:42:27 +02:00
2020-06-24 11:06:09 +02:00
const deleteContents = async ( p ) = > {
2020-06-23 16:42:27 +02:00
for ( const entry of await afs . readdir ( p ) ) {
const curPath = path . join ( p , entry ) ;
await afs . unlink ( curPath ) ;
}
} ;
2020-10-13 15:58:35 +02:00
async function randomArray ( size ) {
return new Promise ( ( resolve , reject ) = > {
crypto . randomBytes ( size , ( err , buf ) = > {
if ( err ) {
reject ( err ) ;
} else {
resolve ( buf . toString ( "base64" ) . replace ( /=+$/g , '' ) ) ;
}
} ) ;
} ) ;
}
2019-12-06 19:17:34 +01:00
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
// it will do any time the auto update poke fails, and there's
// 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 ) {
console . log ( 'Unhandled exception' , error ) ;
} ) ;
let focusHandlerAttached = false ;
ipcMain . on ( 'setBadgeCount' , function ( ev , count ) {
2021-10-11 10:13:30 +02:00
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
// each other. See https://github.com/vector-im/element-web/issues/16942
app . badgeCount = count ;
}
2019-12-06 19:17:34 +01:00
if ( count === 0 && mainWindow ) {
mainWindow . flashFrame ( false ) ;
}
} ) ;
ipcMain . on ( 'loudNotification' , function ( ) {
if ( process . platform === 'win32' && mainWindow && ! mainWindow . isFocused ( ) && ! focusHandlerAttached ) {
mainWindow . flashFrame ( true ) ;
mainWindow . once ( 'focus' , ( ) = > {
mainWindow . flashFrame ( false ) ;
focusHandlerAttached = false ;
} ) ;
focusHandlerAttached = true ;
}
} ) ;
let powerSaveBlockerId = null ;
ipcMain . on ( 'app_onAction' , function ( ev , payload ) {
switch ( payload . action ) {
case 'call_state' :
if ( powerSaveBlockerId !== null && powerSaveBlocker . isStarted ( powerSaveBlockerId ) ) {
if ( payload . state === 'ended' ) {
powerSaveBlocker . stop ( powerSaveBlockerId ) ;
powerSaveBlockerId = null ;
}
} else {
if ( powerSaveBlockerId === null && payload . state === 'connected' ) {
powerSaveBlockerId = powerSaveBlocker . start ( 'prevent-display-sleep' ) ;
}
}
break ;
}
} ) ;
ipcMain . on ( 'ipcCall' , async function ( ev , payload ) {
if ( ! mainWindow ) return ;
const args = payload . args || [ ] ;
let ret ;
switch ( payload . name ) {
case 'getUpdateFeedUrl' :
ret = autoUpdater . getFeedURL ( ) ;
break ;
case 'getAutoLaunchEnabled' :
ret = await launcher . isEnabled ( ) ;
break ;
case 'setAutoLaunchEnabled' :
if ( args [ 0 ] ) {
launcher . enable ( ) ;
} else {
launcher . disable ( ) ;
}
break ;
2021-04-26 14:58:29 +02:00
case 'setLanguage' :
appLocalization . setAppLocale ( args [ 0 ] ) ;
break ;
2021-03-25 15:15:04 +01:00
case 'shouldWarnBeforeExit' :
ret = store . get ( 'warnBeforeExit' , true ) ;
break ;
case 'setWarnBeforeExit' :
store . set ( 'warnBeforeExit' , args [ 0 ] ) ;
break ;
2019-12-06 19:17:34 +01:00
case 'getMinimizeToTrayEnabled' :
ret = tray . hasTray ( ) ;
break ;
case 'setMinimizeToTrayEnabled' :
if ( args [ 0 ] ) {
// Create trayIcon icon
tray . create ( trayConfig ) ;
} else {
tray . destroy ( ) ;
}
store . set ( 'minimizeToTray' , args [ 0 ] ) ;
break ;
case 'getAutoHideMenuBarEnabled' :
2020-03-03 11:00:09 +01:00
ret = global . mainWindow . autoHideMenuBar ;
2019-12-06 19:17:34 +01:00
break ;
case 'setAutoHideMenuBarEnabled' :
store . set ( 'autoHideMenuBar' , args [ 0 ] ) ;
2020-03-05 12:12:01 +01:00
global . mainWindow . autoHideMenuBar = Boolean ( args [ 0 ] ) ;
2019-12-06 19:17:34 +01:00
global . mainWindow . setMenuBarVisibility ( ! args [ 0 ] ) ;
break ;
case 'getAppVersion' :
ret = app . getVersion ( ) ;
break ;
case 'focusWindow' :
if ( mainWindow . isMinimized ( ) ) {
mainWindow . restore ( ) ;
} else if ( ! mainWindow . isVisible ( ) ) {
mainWindow . show ( ) ;
} else {
mainWindow . focus ( ) ;
}
break ;
case 'getConfig' :
ret = vectorConfig ;
break ;
2020-04-14 16:04:58 +02:00
case 'navigateBack' :
if ( mainWindow . webContents . canGoBack ( ) ) {
mainWindow . webContents . goBack ( ) ;
}
break ;
case 'navigateForward' :
if ( mainWindow . webContents . canGoForward ( ) ) {
mainWindow . webContents . goForward ( ) ;
}
break ;
2020-12-01 20:19:12 +01:00
case 'setSpellCheckLanguages' :
2021-02-18 18:05:37 +01:00
if ( args [ 0 ] && args [ 0 ] . length > 0 ) {
2021-04-02 12:07:33 +02:00
mainWindow . webContents . session . setSpellCheckerEnabled ( true ) ;
store . set ( "spellCheckerEnabled" , true ) ;
2021-02-18 18:39:51 +01:00
try {
mainWindow . webContents . session . setSpellCheckerLanguages ( args [ 0 ] ) ;
} catch ( er ) {
console . log ( "There were problems setting the spellcheck languages" , er ) ;
}
2020-12-01 20:27:09 +01:00
} else {
2021-04-02 12:07:33 +02:00
mainWindow . webContents . session . setSpellCheckerEnabled ( false ) ;
store . set ( "spellCheckerEnabled" , false ) ;
2020-02-24 18:16:35 +01:00
}
break ;
2021-02-18 20:12:47 +01:00
case 'getSpellCheckLanguages' :
2021-04-02 12:07:33 +02:00
if ( store . get ( "spellCheckerEnabled" , true ) ) {
ret = mainWindow . webContents . session . getSpellCheckerLanguages ( ) ;
} else {
ret = [ ] ;
}
2021-02-18 20:12:47 +01:00
break ;
2020-11-29 20:50:57 +01:00
case 'getAvailableSpellCheckLanguages' :
ret = mainWindow . webContents . session . availableSpellCheckerLanguages ;
break ;
2020-04-14 14:29:47 +02:00
case 'startSSOFlow' :
recordSSOSession ( args [ 0 ] ) ;
break ;
2019-12-06 19:17:34 +01:00
2020-05-28 21:07:39 +02:00
case 'getPickleKey' :
try {
2020-07-02 14:30:11 +02:00
ret = await keytar . getPassword ( "element.io" , ` ${ args [ 0 ] } | ${ args [ 1 ] } ` ) ;
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
if ( ret === null ) {
ret = await keytar . getPassword ( "riot.im" , ` ${ args [ 0 ] } | ${ args [ 1 ] } ` ) ;
}
2020-05-28 21:07:39 +02:00
} catch ( e ) {
// if an error is thrown (e.g. keytar can't connect to the keychain),
// then return null, which means the default pickle key will be used
ret = null ;
}
break ;
case 'createPickleKey' :
try {
2020-10-13 15:58:35 +02:00
const pickleKey = await randomArray ( 32 ) ;
2020-07-02 14:30:11 +02:00
await keytar . setPassword ( "element.io" , ` ${ args [ 0 ] } | ${ args [ 1 ] } ` , pickleKey ) ;
2020-05-28 21:07:39 +02:00
ret = pickleKey ;
} catch ( e ) {
ret = null ;
}
break ;
case 'destroyPickleKey' :
try {
2020-07-02 14:30:11 +02:00
await keytar . deletePassword ( "element.io" , ` ${ args [ 0 ] } | ${ args [ 1 ] } ` ) ;
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
2020-05-28 21:07:39 +02:00
await keytar . deletePassword ( "riot.im" , ` ${ args [ 0 ] } | ${ args [ 1 ] } ` ) ;
} catch ( e ) { }
break ;
2022-02-11 11:38:19 +01:00
case 'getDesktopCapturerSources' :
ret = ( await desktopCapturer . getSources ( args [ 0 ] ) ) . map ( ( source ) = > ( {
id : source.id ,
name : source.name ,
thumbnailURL : source.thumbnail.toDataURL ( ) ,
} ) ) ;
break ;
2020-05-28 21:07:39 +02:00
2019-12-06 19:17:34 +01:00
default :
mainWindow . webContents . send ( 'ipcReply' , {
id : payload.id ,
error : "Unknown IPC Call: " + payload . name ,
} ) ;
return ;
}
mainWindow . webContents . send ( 'ipcReply' , {
id : payload.id ,
reply : ret ,
} ) ;
} ) ;
2021-04-23 16:17:50 +02:00
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE" ;
async function getOrCreatePassphrase ( key ) {
if ( keytar ) {
try {
const storedPassphrase = await keytar . getPassword ( "element.io" , key ) ;
if ( storedPassphrase !== null ) {
return storedPassphrase ;
} else {
2021-04-23 16:35:09 +02:00
const newPassphrase = await randomArray ( 32 ) ;
await keytar . setPassword ( "element.io" , key , newPassphrase ) ;
return newPassphrase ;
2021-04-23 16:17:50 +02:00
}
} catch ( e ) {
console . log ( "Error getting the event index passphrase out of the secret store" , e ) ;
}
} else {
return seshatDefaultPassphrase ;
}
}
2019-12-06 19:17:34 +01:00
ipcMain . on ( 'seshat' , async function ( ev , payload ) {
if ( ! mainWindow ) return ;
const sendError = ( id , e ) = > {
const error = {
2019-12-10 19:10:15 +01:00
message : e.message ,
} ;
2019-12-06 19:17:34 +01:00
mainWindow . webContents . send ( 'seshatReply' , {
2019-12-10 19:10:15 +01:00
id : id ,
error : error ,
2019-12-06 19:17:34 +01:00
} ) ;
2019-12-10 19:10:15 +01:00
} ;
2019-12-06 19:17:34 +01:00
const args = payload . args || [ ] ;
let ret ;
switch ( payload . name ) {
case 'supportsEventIndexing' :
2020-03-11 11:27:00 +01:00
ret = seshatSupported ;
2019-12-06 19:17:34 +01:00
break ;
case 'initEventIndex' :
if ( eventIndex === null ) {
2020-10-13 17:07:39 +02:00
const userId = args [ 0 ] ;
const deviceId = args [ 1 ] ;
const passphraseKey = ` seshat| ${ userId } | ${ deviceId } ` ;
2021-04-23 16:17:50 +02:00
const passphrase = await getOrCreatePassphrase ( passphraseKey ) ;
2020-10-13 15:59:21 +02:00
try {
2021-05-27 15:39:26 +02:00
await afs . mkdir ( eventStorePath , { recursive : true } ) ;
eventIndex = new Seshat ( eventStorePath , { passphrase } ) ;
2020-10-13 17:07:39 +02:00
} catch ( e ) {
2021-04-23 16:17:50 +02:00
if ( e instanceof ReindexError ) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery ( eventStorePath , {
passphrase ,
} ) ;
2020-10-13 15:59:21 +02:00
2021-04-23 16:17:50 +02:00
const userVersion = await recoveryIndex . getUserVersion ( ) ;
2020-10-13 15:59:21 +02:00
2021-04-23 16:17:50 +02:00
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if ( userVersion === 0 ) {
await recoveryIndex . shutdown ( ) ;
2020-10-13 15:59:21 +02:00
2021-04-23 16:17:50 +02:00
try {
await deleteContents ( eventStorePath ) ;
} catch ( e ) {
}
} else {
await recoveryIndex . reindex ( ) ;
}
2021-04-21 13:20:29 +02:00
2021-05-27 15:39:26 +02:00
eventIndex = new Seshat ( eventStorePath , { passphrase } ) ;
2021-04-23 16:17:50 +02:00
} else {
2020-03-11 11:27:00 +01:00
sendError ( payload . id , e ) ;
return ;
}
2019-12-06 19:17:34 +01:00
}
}
break ;
case 'closeEventIndex' :
2020-03-13 11:32:25 +01:00
if ( eventIndex !== null ) {
2020-03-24 14:23:49 +01:00
const index = eventIndex ;
2020-03-24 14:16:54 +01:00
eventIndex = null ;
2020-03-13 11:32:25 +01:00
try {
2020-03-24 14:16:54 +01:00
await index . shutdown ( ) ;
2020-03-13 11:32:25 +01:00
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
2019-12-06 19:17:34 +01:00
break ;
case 'deleteEventIndex' :
2019-12-10 19:10:15 +01:00
{
try {
2020-06-24 11:06:09 +02:00
await deleteContents ( eventStorePath ) ;
2019-12-10 19:10:15 +01:00
} catch ( e ) {
}
2019-12-06 19:17:34 +01:00
}
break ;
case 'isEventIndexEmpty' :
if ( eventIndex === null ) ret = true ;
else ret = await eventIndex . isEmpty ( ) ;
break ;
2020-06-08 16:41:15 +02:00
case 'isRoomIndexed' :
if ( eventIndex === null ) ret = false ;
else ret = await eventIndex . isRoomIndexed ( args [ 0 ] ) ;
break ;
2019-12-06 19:17:34 +01:00
case 'addEventToIndex' :
try {
eventIndex . addEvent ( args [ 0 ] , args [ 1 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
break ;
2020-03-12 11:46:14 +01:00
case 'deleteEvent' :
try {
ret = await eventIndex . deleteEvent ( args [ 0 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
break ;
2019-12-06 19:17:34 +01:00
case 'commitLiveEvents' :
try {
ret = await eventIndex . commit ( ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
break ;
case 'searchEventIndex' :
try {
ret = await eventIndex . search ( args [ 0 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
break ;
case 'addHistoricEvents' :
if ( eventIndex === null ) ret = false ;
else {
try {
ret = await eventIndex . addHistoricEvents (
args [ 0 ] , args [ 1 ] , args [ 2 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
2020-02-03 17:23:42 +01:00
case 'getStats' :
if ( eventIndex === null ) ret = 0 ;
else {
try {
ret = await eventIndex . getStats ( ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
2019-12-06 19:17:34 +01:00
case 'removeCrawlerCheckpoint' :
if ( eventIndex === null ) ret = false ;
else {
try {
ret = await eventIndex . removeCrawlerCheckpoint ( args [ 0 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
case 'addCrawlerCheckpoint' :
if ( eventIndex === null ) ret = false ;
else {
try {
ret = await eventIndex . addCrawlerCheckpoint ( args [ 0 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
2020-02-03 17:21:35 +01:00
case 'loadFileEvents' :
if ( eventIndex === null ) ret = [ ] ;
else {
try {
ret = await eventIndex . loadFileEvents ( args [ 0 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
2019-12-06 19:17:34 +01:00
case 'loadCheckpoints' :
if ( eventIndex === null ) ret = [ ] ;
else {
try {
ret = await eventIndex . loadCheckpoints ( ) ;
} catch ( e ) {
ret = [ ] ;
}
}
break ;
2020-06-17 17:19:15 +02:00
case 'setUserVersion' :
if ( eventIndex === null ) break ;
else {
try {
await eventIndex . setUserVersion ( args [ 0 ] ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
case 'getUserVersion' :
if ( eventIndex === null ) ret = 0 ;
else {
try {
ret = await eventIndex . getUserVersion ( ) ;
} catch ( e ) {
sendError ( payload . id , e ) ;
return ;
}
}
break ;
2019-12-06 19:17:34 +01:00
default :
mainWindow . webContents . send ( 'seshatReply' , {
id : payload.id ,
error : "Unknown IPC Call: " + payload . name ,
} ) ;
return ;
}
mainWindow . webContents . send ( 'seshatReply' , {
id : payload.id ,
reply : ret ,
} ) ;
} ) ;
app . commandLine . appendSwitch ( '--enable-usermedia-screen-capturing' ) ;
2021-09-06 16:18:16 +02:00
if ( ! app . commandLine . hasSwitch ( 'enable-features' ) ) {
app . commandLine . appendSwitch ( 'enable-features' , 'WebRTCPipeWireCapturer' ) ;
}
2019-12-06 19:17:34 +01:00
const gotLock = app . requestSingleInstanceLock ( ) ;
if ( ! gotLock ) {
console . log ( 'Other instance detected: exiting' ) ;
app . exit ( ) ;
}
2020-03-02 16:04:51 +01:00
// do this after we know we are the primary instance of the app
protocolInit ( ) ;
2019-12-06 19:17:34 +01:00
// Register the scheme the app is served from as 'standard'
// which allows things like relative URLs and IndexedDB to
// work.
// Also mark it as secure (ie. accessing resources from this
// protocol and HTTPS won't trigger mixed content warnings).
protocol . registerSchemesAsPrivileged ( [ {
scheme : 'vector' ,
privileges : {
standard : true ,
secure : true ,
supportFetchAPI : true ,
} ,
} ] ) ;
2020-05-21 00:16:57 +02:00
// Turn the sandbox on for *all* windows we might generate. Doing this means we don't
// have to specify a `sandbox: true` to each BrowserWindow.
//
// This also fixes an issue with window.open where if we only specified the sandbox
// on the main window we'd run into cryptic "ipc_renderer be broke" errors. Turns out
// it's trying to jump the sandbox and make some calls into electron, which it can't
// do when half of it is sandboxed. By turning on the sandbox for everything, the new
// window (no matter how temporary it may be) is also sandboxed, allowing for a clean
// transition into the user's browser.
app . enableSandbox ( ) ;
2021-04-03 14:10:11 +02:00
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
2021-04-02 16:27:32 +02:00
app . commandLine . appendSwitch ( 'disable-features' , 'HardwareMediaKeyHandling,MediaSessionService' ) ;
2020-05-21 00:16:57 +02:00
2019-12-10 18:40:17 +01:00
app . on ( 'ready' , async ( ) = > {
try {
await setupGlobals ( ) ;
2020-07-02 14:30:11 +02:00
await moveAutoLauncher ( ) ;
2019-12-10 18:40:17 +01:00
} catch ( e ) {
console . log ( "App setup failed: exiting" , e ) ;
process . exit ( 1 ) ;
// process.exit doesn't cause node to stop running code immediately,
// so return (we could let the exception propagate but then we end up
// with node printing all sorts of stuff about unhandled exceptions
// when we want the actual error to be as obvious as possible).
return ;
}
2019-12-06 19:17:34 +01:00
if ( argv [ 'devtools' ] ) {
try {
2021-07-01 10:24:02 +02:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2019-12-06 19:17:34 +01:00
const { default : installExt , REACT_DEVELOPER_TOOLS , REACT_PERF } = require ( 'electron-devtools-installer' ) ;
installExt ( REACT_DEVELOPER_TOOLS )
. then ( ( name ) = > console . log ( ` Added Extension: ${ name } ` ) )
. catch ( ( err ) = > console . log ( 'An error occurred: ' , err ) ) ;
installExt ( REACT_PERF )
. then ( ( name ) = > console . log ( ` Added Extension: ${ name } ` ) )
. catch ( ( err ) = > console . log ( 'An error occurred: ' , err ) ) ;
} catch ( e ) {
console . log ( e ) ;
}
}
protocol . registerFileProtocol ( 'vector' , ( request , callback ) = > {
if ( request . method !== 'GET' ) {
2021-05-27 15:39:26 +02:00
callback ( { error : - 322 } ) ; // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
2019-12-06 19:17:34 +01:00
return null ;
}
const parsedUrl = new URL ( request . url ) ;
if ( parsedUrl . protocol !== 'vector:' ) {
2021-05-27 15:39:26 +02:00
callback ( { error : - 302 } ) ; // UNKNOWN_URL_SCHEME
2019-12-06 19:17:34 +01:00
return ;
}
if ( parsedUrl . host !== 'vector' ) {
2021-05-27 15:39:26 +02:00
callback ( { error : - 105 } ) ; // NAME_NOT_RESOLVED
2019-12-06 19:17:34 +01:00
return ;
}
const target = parsedUrl . pathname . split ( '/' ) ;
// path starts with a '/'
if ( target [ 0 ] !== '' ) {
2021-05-27 15:39:26 +02:00
callback ( { error : - 6 } ) ; // FILE_NOT_FOUND
2019-12-06 19:17:34 +01:00
return ;
}
if ( target [ target . length - 1 ] == '' ) {
target [ target . length - 1 ] = 'index.html' ;
}
let baseDir ;
2019-12-09 13:49:41 +01:00
if ( target [ 1 ] === 'webapp' ) {
2019-12-10 18:40:17 +01:00
baseDir = asarPath ;
2019-12-06 19:17:34 +01:00
} else {
2021-05-27 15:39:26 +02:00
callback ( { error : - 6 } ) ; // FILE_NOT_FOUND
2019-12-06 19:17:34 +01:00
return ;
}
// Normalise the base dir and the target path separately, then make sure
// the target path isn't trying to back out beyond its root
baseDir = path . normalize ( baseDir ) ;
const relTarget = path . normalize ( path . join ( . . . target . slice ( 2 ) ) ) ;
if ( relTarget . startsWith ( '..' ) ) {
2021-05-27 15:39:26 +02:00
callback ( { error : - 6 } ) ; // FILE_NOT_FOUND
2019-12-06 19:17:34 +01:00
return ;
}
const absTarget = path . join ( baseDir , relTarget ) ;
callback ( {
path : absTarget ,
} ) ;
} ) ;
if ( argv [ 'no-update' ] ) {
console . log ( 'Auto update disabled via command line flag "--no-update"' ) ;
} else if ( vectorConfig [ 'update_base_url' ] ) {
console . log ( ` Starting auto update with base URL: ${ vectorConfig [ 'update_base_url' ] } ` ) ;
updater . start ( vectorConfig [ 'update_base_url' ] ) ;
} else {
console . log ( 'No update_base_url is defined: auto update is disabled' ) ;
}
// Load the previous window state with fallback to defaults
const mainWindowState = windowStateKeeper ( {
defaultWidth : 1024 ,
defaultHeight : 768 ,
} ) ;
const preloadScript = path . normalize ( ` ${ __dirname } /preload.js ` ) ;
mainWindow = global . mainWindow = new BrowserWindow ( {
2020-02-21 12:17:18 +01:00
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor : '#fff' ,
2019-12-06 19:17:34 +01:00
icon : iconPath ,
show : false ,
autoHideMenuBar : store.get ( 'autoHideMenuBar' , true ) ,
x : mainWindowState.x ,
y : mainWindowState.y ,
width : mainWindowState.width ,
height : mainWindowState.height ,
webPreferences : {
preload : preloadScript ,
nodeIntegration : false ,
2020-05-21 00:16:57 +02:00
//sandbox: true, // We enable sandboxing from app.enableSandbox() above
2021-01-13 16:21:00 +01:00
contextIsolation : true ,
2021-12-10 15:55:35 +01:00
webgl : true ,
2019-12-06 19:17:34 +01:00
} ,
} ) ;
mainWindow . loadURL ( 'vector://vector/webapp/' ) ;
2021-04-02 12:07:33 +02:00
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted so we have to use the store here
mainWindow . webContents . session . setSpellCheckerEnabled ( store . get ( "spellCheckerEnabled" , true ) ) ;
2019-12-06 19:17:34 +01:00
// Create trayIcon icon
if ( store . get ( 'minimizeToTray' , true ) ) tray . create ( trayConfig ) ;
mainWindow . once ( 'ready-to-show' , ( ) = > {
mainWindowState . manage ( mainWindow ) ;
if ( ! argv [ 'hidden' ] ) {
mainWindow . show ( ) ;
} else {
// hide here explicitly because window manage above sometimes shows it
mainWindow . hide ( ) ;
}
} ) ;
2021-03-31 09:58:24 +02:00
mainWindow . webContents . on ( 'before-input-event' , warnBeforeExit ) ;
2021-03-29 13:10:27 +02:00
2019-12-06 19:17:34 +01:00
mainWindow . on ( 'closed' , ( ) = > {
mainWindow = global . mainWindow = null ;
} ) ;
2021-03-25 13:46:10 +01:00
mainWindow . on ( 'close' , async ( e ) = > {
2021-03-25 15:50:33 +01:00
// If we are not quitting and have a tray icon then minimize to tray
if ( ! global . appQuitting && ( tray . hasTray ( ) || process . platform === 'darwin' ) ) {
// On Mac, closing the window just hides it
// (this is generally how single-window Mac apps
// behave, eg. Mail.app)
e . preventDefault ( ) ;
2021-05-05 04:55:28 +02:00
if ( mainWindow . isFullScreen ( ) ) {
mainWindow . once ( 'leave-full-screen' , ( ) = > mainWindow . hide ( ) ) ;
mainWindow . setFullScreen ( false ) ;
} else {
mainWindow . hide ( ) ;
}
2021-03-25 15:50:33 +01:00
return false ;
}
2019-12-06 19:17:34 +01:00
} ) ;
if ( process . platform === 'win32' ) {
// Handle forward/backward mouse buttons in Windows
mainWindow . on ( 'app-command' , ( e , cmd ) = > {
if ( cmd === 'browser-backward' && mainWindow . webContents . canGoBack ( ) ) {
mainWindow . webContents . goBack ( ) ;
} else if ( cmd === 'browser-forward' && mainWindow . webContents . canGoForward ( ) ) {
mainWindow . webContents . goForward ( ) ;
}
} ) ;
}
webContentsHandler ( mainWindow . webContents ) ;
2021-04-26 14:58:29 +02:00
appLocalization = new AppLocalization ( {
store ,
components : [
( ) = > tray . initApplicationMenu ( ) ,
( ) = > Menu . setApplicationMenu ( buildMenuTemplate ( ) ) ,
] ,
} ) ;
2019-12-06 19:17:34 +01:00
} ) ;
app . on ( 'window-all-closed' , ( ) = > {
app . quit ( ) ;
} ) ;
app . on ( 'activate' , ( ) = > {
mainWindow . show ( ) ;
} ) ;
2020-05-23 12:52:49 +02:00
function beforeQuit() {
2019-12-06 19:17:34 +01:00
global . appQuitting = true ;
if ( mainWindow ) {
mainWindow . webContents . send ( 'before-quit' ) ;
}
2020-05-23 12:52:49 +02:00
}
app . on ( 'before-quit' , beforeQuit ) ;
2021-06-25 15:35:58 +02:00
autoUpdater . on ( 'before-quit-for-update' , beforeQuit ) ;
2019-12-06 19:17:34 +01:00
app . on ( 'second-instance' , ( ev , commandLine , workingDirectory ) = > {
// If other instance launched with --hidden then skip showing window
if ( commandLine . includes ( '--hidden' ) ) return ;
// Someone tried to run a second instance, we should focus our window.
if ( mainWindow ) {
if ( ! mainWindow . isVisible ( ) ) mainWindow . show ( ) ;
if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
mainWindow . focus ( ) ;
}
} ) ;
// Set the App User Model ID to match what the squirrel
// installer uses for the shortcut icon.
// This makes notifications work on windows 8.1 (and is
// a noop on other platforms).
2020-07-01 16:30:53 +02:00
app . setAppUserModelId ( 'com.squirrel.element-desktop.Element' ) ;