SDA-4177 Adding tel custom protocol to support calls through Symphony on windows and mac

This commit is contained in:
sbenmoussati 2023-05-10 14:40:18 +02:00 committed by Salah Benmoussati
parent 1864a7b410
commit 36d6e51221
14 changed files with 977 additions and 19780 deletions

View File

@ -75,7 +75,7 @@ npm run test:unit -- --match=spell*
```
registry = https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/
_auth = <auth value from #4>
//repo.symphony.com/artifactory/api/npm/npm-virtual-dev/:_auth = <auth value from #4>
always-auth = true
```

20407
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -74,15 +74,19 @@
"entitlements": "entitlements.mac.plist",
"notarize": false,
"entitlementsInherit": "entitlements.mac.plist",
"extendInfo": {
"CFBundleURLTypes": [
{
"CFBundleTypeRole": "Viewer",
"CFBundleURLName": "SymTel",
"CFBundleURLSchemes": [
"tel"
]
}
]
},
"gatekeeperAssess": true,
"hardenedRuntime": true,
"binaries": [
"library/lz4.exec",
"library/indexvalidator.exec",
"library/libsymphonysearch.dylib",
"library/cryptoLib.dylib",
"library/dictionary"
],
"target": [
"dir",
"zip"

View File

@ -67,18 +67,6 @@ fi
NODE_VERSION=$(node --version)
echo "Executing using Node Version: ${NODE_VERSION}"
# We need to include swift search libraries to build SDA
if [ ! -d "$HOME/tronlibraries/library" ]; then
echo 'Search libraries do not exist! Not building with swift search' >&2
else
cp -r "$HOME/tronlibraries/library" .
echo 'Copied search libraries'
ls -lrth $HOME/tronlibraries/library
fi
codesign --force --options runtime -s "Developer ID Application: Symphony Communication Services LLC" library/lz4.exec
codesign --force --options runtime -s "Developer ID Application: Symphony Communication Services LLC" library/indexvalidator.exec
PKG_VERSION=$(node -e "console.log(require('./package.json').version);")
# Install app dependencies

View File

@ -48,9 +48,6 @@ call snyk config set api=%SNYK_API_TOKEN%
:: Below command replaces buildVersion with the appropriate build number from jenkins
sed -i -e "s/\"buildNumber\"[[:space:]]*\:[[:space:]]*\".*\"/\"buildNumber\":\"%PARENT_BUILD_VERSION%\"/g" package.json
:: Copy search libraries onto the project root
echo "Copying search libraries"
echo D | xcopy /y "C:\jenkins\workspace\tronlibraries\library" "library"
echo "Installing dependencies..."
call npm install

View File

@ -41,10 +41,6 @@ call snyk config set api=%SNYK_API_TOKEN%
sed -i -e "s/\"buildNumber\"[[:space:]]*\:[[:space:]]*\".*\"/\"buildNumber\": \"%PARENT_BUILD_VERSION%\"/g" package.json
sed -i -e "s/\"version\"[[:space:]]*\:[[:space:]]\"\(.*\)\"/\"version\": \"\1-%PARENT_BUILD_VERSION%\"/g" package.json
:: Copy search libraries onto the project root
echo "Copying search libraries"
echo D | xcopy /y "C:\jenkins\workspace\tronlibraries\library" "library"
echo "Installing dependencies..."
call npm install
@ -138,23 +134,6 @@ IF %errorlevel% neq 0 (
exit /b -1
)
call %SIGNING_FILE_PATH% ..\..\library\indexvalidator-x64.exe
IF %errorlevel% neq 0 (
echo "Signing failed"
exit /b -1
)
call %SIGNING_FILE_PATH% ..\..\library\lz4-win-x64.exe
IF %errorlevel% neq 0 (
echo "Signing failed"
exit /b -1
)
call %SIGNING_FILE_PATH% ..\..\library\tar-win.exe
IF %errorlevel% neq 0 (
echo "Signing failed"
exit /b -1
)
node ..\..\scripts\windows_update_checksum.js "..\..\dist\Symphony-%SYMVER%-win.exe" "..\..\dist\latest.yml"

View File

@ -53,6 +53,7 @@ import { getCommandLineArgs } from '../common/utils';
import { autoUpdate, AutoUpdateTrigger } from './auto-update-handler';
import { presenceStatus } from './presence-status-handler';
import { presenceStatusStore } from './stores/index';
import { voiceHandler } from './voice-handler';
// Swift search API
let swiftSearchInstance;
@ -492,7 +493,8 @@ ipcMain.on(
autoUpdate.checkUpdates();
}
break;
default:
case apiCmds.registerVoiceServices:
voiceHandler.registerSymphonyAsDefaultCallApp();
break;
}
},

View File

@ -83,7 +83,6 @@ const startApplication = async () => {
await config.initializeUserConfig();
await config.readUserConfig();
await config.checkFirstTimeLaunch();
if (config.isFirstTimeLaunch()) {
logger.info(
`main: This is a first time launch! will update config and handle auto launch`,

View File

@ -10,6 +10,7 @@ import { windowHandler } from './window-handler';
enum protocol {
SymphonyProtocol = 'symphony://',
TelProtocol = 'tel:',
}
class ProtocolHandler {
@ -31,12 +32,12 @@ class ProtocolHandler {
public setPreloadWebContents(webContents: WebContents): void {
this.preloadWebContents = webContents;
logger.info(
`protocol handler: SFE is active and we have a valid protocol window with web contents!`,
`protocol-handler: SFE is active and we have a valid protocol window with web contents!`,
);
if (this.protocolUri) {
this.sendProtocol(this.protocolUri);
logger.info(
`protocol handler: we have a cached url ${this.protocolUri}, so, processed the request to SFE!`,
`protocol-handler: we have a cached url ${this.protocolUri}, so, processed the request to SFE!`,
);
this.protocolUri = null;
}
@ -59,7 +60,7 @@ class ProtocolHandler {
return;
}
logger.info(
`protocol handler: processing protocol request for the url ${url}!`,
`protocol-handler: processing protocol request for the url ${url}!`,
);
// Handle protocol for Seamless login
if (url?.includes('skey') && url?.includes('anticsrf')) {
@ -69,7 +70,7 @@ class ProtocolHandler {
if (!this.preloadWebContents || !isAppRunning) {
logger.info(
`protocol handler: app was started from the protocol request. Caching the URL ${url}!`,
`protocol-handler: app was started from the protocol request. Caching the URL ${url}!`,
);
this.protocolUri = url;
return;
@ -83,9 +84,14 @@ class ProtocolHandler {
if (ProtocolHandler.isValidProtocolUri(url)) {
logger.info(
`protocol handler: our protocol request is a valid url ${url}! sending request to SFE for further action!`,
`protocol-handler: our protocol request is a valid url ${url}! sending request to SFE for further action!`,
);
this.preloadWebContents.send('protocol-action', url);
} else if (url?.includes('tel:')) {
this.preloadWebContents.send(
'phone-number-received',
url.split('tel:')[1],
);
}
}
@ -95,17 +101,27 @@ class ProtocolHandler {
* @param argv {String[]} - data received from process.argv
*/
public processArgv(argv?: string[], isAppAlreadyOpen: boolean = false): void {
logger.info(`protocol handler: processing protocol args!`);
logger.info(`protocol-handler: processing protocol args!`);
const protocolUriFromArgv = getCommandLineArgs(
argv || process.argv,
protocol.SymphonyProtocol,
false,
);
const telArgFromArgv = getCommandLineArgs(
argv || process.argv,
protocol.TelProtocol,
false,
);
if (protocolUriFromArgv) {
logger.info(
`protocol handler: we have a protocol request for the url ${protocolUriFromArgv}!`,
`protocol-handler: we have a protocol request for the url ${protocolUriFromArgv}!`,
);
this.sendProtocol(protocolUriFromArgv, isAppAlreadyOpen);
} else if (telArgFromArgv) {
logger.info(
`protocol-handler: we have a tel request for ${telArgFromArgv}!`,
);
this.sendProtocol(telArgFromArgv, isAppAlreadyOpen);
}
}

222
src/app/voice-handler.ts Normal file
View File

@ -0,0 +1,222 @@
import { exec } from 'child_process';
import { app } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { isDevEnv, isMac, isWindowsOS } from '../common/env';
import { logger } from '../common/logger';
const LS_REGISTER_PATH =
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
const SYM_TEL_PROTOCOL_PLIST_ENTRY = {
LSHandlerURLScheme: 'tel',
LSHandlerRoleAll: 'com.symphony.electron-desktop',
LSHandlerPreferredVersions: {
LSHandlerRoleAll: '-',
},
};
const DEFAULT_TEL_PROTOCOL = 'tel';
enum REGISTRY_PATHS {
Capabilities = '\\Software\\Symphony\\Capabilities',
UrlRegistration = '\\Software\\Symphony\\Capabilities\\URLAssociations',
SymTelCmd = '\\Software\\Classes\\Symphony.tel\\shell\\open\\command',
SymTelDefaultIcon = '\\Software\\Classes\\Symphony.tel\\DefaultIcon',
RegisteredApps = '\\Software\\RegisteredApplications',
}
class VoiceHandler {
/**
* Registers Symphony as phone calls app
*/
public registerSymphonyAsDefaultCallApp() {
if (isWindowsOS) {
this.registerAppOnWindows();
} else if (isMac) {
this.registerAppOnMacOS();
}
}
/**
* Registers app on Windows
*/
private async registerAppOnWindows() {
const Registry = require('winreg');
const appPath = isDevEnv
? path.join(path.dirname(app.getPath('exe')), 'Electron.exe')
: path.join(path.dirname(app.getPath('exe')), 'Symphony.exe');
const applicationCapabilitiesRegKey = new Registry({
hive: Registry.HKCU,
key: REGISTRY_PATHS.Capabilities,
});
const symURLAssociationRegKey = new Registry({
hive: Registry.HKCU,
key: REGISTRY_PATHS.UrlRegistration,
});
const symTelCommandRegKey = new Registry({
hive: Registry.HKCU,
key: REGISTRY_PATHS.SymTelCmd,
});
const symTelDefaultIconRegKey = new Registry({
hive: Registry.HKCU,
key: REGISTRY_PATHS.SymTelDefaultIcon,
});
const symAppRegistrationRegKey = new Registry({
hive: Registry.HKCU,
key: REGISTRY_PATHS.RegisteredApps,
});
const errorCallback = (error) => {
if (error) {
logger.error(
'voice-handler: error while creating voice registry keys: ',
error,
);
}
};
await applicationCapabilitiesRegKey.set(
'ApplicationName',
Registry.REG_SZ,
'Symphony',
errorCallback,
);
await applicationCapabilitiesRegKey.set(
'ApplicationDescription',
Registry.REG_SZ,
'Symphony',
errorCallback,
);
await symURLAssociationRegKey.set(
'tel',
Registry.REG_SZ,
'Symphony.tel',
errorCallback,
);
await symTelDefaultIconRegKey.set(
'',
Registry.REG_SZ,
appPath,
errorCallback,
);
await symTelCommandRegKey.set(
'',
Registry.REG_SZ,
`"${appPath}" "%1"`,
errorCallback,
);
await symAppRegistrationRegKey.set(
'Symphony',
Registry.REG_SZ,
'Software\\Symphony\\Capabilities',
errorCallback,
);
}
/**
* Registers app on macOS
*/
private registerAppOnMacOS() {
this.readLaunchServicesPlist((res, _err) => {
const data = res;
const itemIdx = data.findIndex(
(item) => item.LSHandlerURLScheme === DEFAULT_TEL_PROTOCOL,
);
// macOS allows only one app being declared as able to make calls
if (itemIdx !== -1) {
data.splice(itemIdx, 1);
}
data.push(SYM_TEL_PROTOCOL_PLIST_ENTRY);
this.updateLaunchServicesPlist(data);
});
}
/**
* Reads macOS launch services plist
* @param callback
*/
private readLaunchServicesPlist(callback) {
const plistPath = this.getLaunchServicesPlistPath();
const tmpPath = `${plistPath}.${Math.random()}`;
exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, (err) => {
if (err) {
callback(err);
return;
}
fs.readFile(tmpPath, (readErr, data) => {
if (readErr) {
callback(readErr);
return;
}
try {
const json = JSON.parse(data.toString());
callback(json.LSHandlers, json);
fs.unlink(tmpPath, (err) => {
logger.error('Error: ', err);
});
} catch (e) {
callback(e);
}
});
});
}
/**
* Updates launch services plist file
* @param defaults
*/
private updateLaunchServicesPlist(defaults) {
const plistPath = this.getLaunchServicesPlistPath();
const tmpPath = `${plistPath}.${Math.random()}`;
exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, (err) => {
if (err) {
logger.error('voice-handler: error while converting plist ', err);
return;
}
try {
let data = fs.readFileSync(tmpPath).toString();
data = JSON.parse(data);
(data as any).LSHandlers = defaults;
data = JSON.stringify(data);
fs.writeFileSync(tmpPath, data);
} catch (e) {
logger.error('voice-handler: error while converting plist ', err);
return;
}
exec(`plutil -convert binary1 "${tmpPath}" -o "${plistPath}"`, () => {
fs.unlink(tmpPath, (err) => {
logger.error(`voice-handler: error while clearing ${tmpPath}: `, err);
});
// Relaunch Launch Services so it take into consideration updated plist file
exec(
`${LS_REGISTER_PATH} -kill -r -domain local -domain system -domain user`,
(registerErr) => {
if (registerErr) {
logger.error(
'voice-handler: error relaunching Launch Services ',
registerErr,
);
}
},
);
});
});
}
/**
* Returns Launch services plist filepath
* @param callback
*/
private getLaunchServicesPlistPath() {
const secureLaunchServicesPlist = `${process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist`;
const insecureLaunchServicesPlist = `${process.env.HOME}/Library/Preferences/com.apple.LaunchServices.plist`;
const secureLaunchServicesPlistExists = fs.existsSync(
secureLaunchServicesPlist,
);
if (secureLaunchServicesPlistExists) {
return secureLaunchServicesPlist;
}
return insecureLaunchServicesPlist;
}
}
const voiceHandler = new VoiceHandler();
export { voiceHandler };

View File

@ -547,6 +547,7 @@ export const handlePermissionRequests = (webContents: WebContents): void => {
case Permissions.OPEN_EXTERNAL:
if (
details?.externalURL?.startsWith('symphony:') ||
details?.externalURL?.startsWith('tel:') ||
details?.externalURL?.startsWith('mailto:')
) {
return callback(true);

View File

@ -76,6 +76,7 @@ export enum apiCmds {
updateMyPresence = 'update-my-presence',
getMyPresence = 'get-my-presence',
updateSymphonyTray = 'update-system-tray',
registerVoiceServices = 'register-voice-services',
}
export enum apiName {

View File

@ -99,6 +99,7 @@ if (ssfWindow.ssf) {
checkForUpdates: ssfWindow.ssf.checkForUpdates,
updateMyPresence: ssfWindow.ssf.updateMyPresence,
getMyPresence: ssfWindow.ssf.getMyPresence,
registerVoiceServices: ssfWindow.ssf.registerVoiceServices,
});
}

View File

@ -61,6 +61,7 @@ export interface ILocalObject {
c9PipeEventCallback?: (event: string, arg?: any) => void;
c9MessageCallback?: (status: IShellStatus) => void;
updateMyPresenceCallback?: (presence: EPresenceStatusCategory) => void;
phoneNumberCallback?: (arg: string) => void;
}
const local: ILocalObject = {
@ -876,6 +877,21 @@ export class SSFApi {
autoUpdateTrigger,
});
}
/**
* Allows JS to register SDA for calls
* @param {Function} phoneNumberCallback callback function invoked when receiving a phone number
*/
public registerVoiceServices(
phoneNumberCallback: (arg: string) => void,
): void {
if (typeof phoneNumberCallback === 'function') {
local.phoneNumberCallback = phoneNumberCallback;
}
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.registerVoiceServices,
});
}
}
/**
@ -1145,6 +1161,24 @@ local.ipcRenderer.on('c9-status-event', (_event, args) => {
local.c9MessageCallback?.call(null, args?.status);
});
/**
* An event triggered by the main process
* to forward clicked phone number
*
* @param {string} phoneNumber - phone number received by SDA
*/
local.ipcRenderer.on(
'phone-number-received',
(_event: Event, phoneNumber: string) => {
if (
typeof phoneNumber === 'string' &&
typeof local.phoneNumberCallback === 'function'
) {
local.phoneNumberCallback(phoneNumber);
}
},
);
// Invoked whenever the app is reloaded/navigated
const sanitize = (): void => {
if (window.name === apiName.mainWindowName) {