SDA-4178 Add support for SMS protocol

This commit is contained in:
sbenmoussati 2023-07-12 09:36:00 +02:00 committed by Salah Benmoussati
parent 028292b6d4
commit 3d5519aea0
8 changed files with 248 additions and 120 deletions

View File

@ -484,6 +484,7 @@ public class CustomActions
{ {
key.DeleteSubKeyTree("symphony", false); key.DeleteSubKeyTree("symphony", false);
key.DeleteSubKeyTree("Symphony.tel", false); key.DeleteSubKeyTree("Symphony.tel", false);
key.DeleteSubKeyTree("Symphony.sms", false);
} }
} }

View File

@ -57,7 +57,7 @@ const verifyProtocolForNewUrl = (url: string): boolean => {
return false; return false;
} }
const allowedProtocols = ['http:', 'https:', 'mailto:', 'symphony:']; const allowedProtocols = ['http:', 'https:', 'mailto:', 'symphony:', 'sms:'];
// url parse returns protocol with : // url parse returns protocol with :
if (allowedProtocols.includes(parsedUrl.protocol)) { if (allowedProtocols.includes(parsedUrl.protocol)) {
logger.info( logger.info(
@ -207,7 +207,8 @@ export const handleChildWindow = (webContents: WebContents): void => {
webContents.on( webContents.on(
'did-create-window', 'did-create-window',
(browserWindow: BrowserWindow, details: DidCreateWindowDetails) => { (browserWindow: BrowserWindow, details: DidCreateWindowDetails) => {
const newWinOptions = details.options as ICustomBrowserWindowConstructorOpts; const newWinOptions =
details.options as ICustomBrowserWindowConstructorOpts;
const width = newWinOptions.width || DEFAULT_POP_OUT_WIDTH; const width = newWinOptions.width || DEFAULT_POP_OUT_WIDTH;
const height = newWinOptions.height || DEFAULT_POP_OUT_HEIGHT; const height = newWinOptions.height || DEFAULT_POP_OUT_HEIGHT;
const newWinKey = getGuid(); const newWinKey = getGuid();

View File

@ -496,8 +496,13 @@ ipcMain.on(
autoUpdate.checkUpdates(); autoUpdate.checkUpdates();
} }
break; break;
case apiCmds.registerVoiceServices: case apiCmds.registerPhoneNumberServices:
voiceHandler.registerSymphonyAsDefaultCallApp(); voiceHandler.registerSymphonyAsDefaultApp(arg.protocols);
break;
case apiCmds.unregisterPhoneNumberServices:
voiceHandler.unregisterSymphonyAsDefaultApp(arg.protocols);
break;
default:
break; break;
} }
}, },

View File

@ -11,6 +11,7 @@ import { windowHandler } from './window-handler';
enum protocol { enum protocol {
SymphonyProtocol = 'symphony://', SymphonyProtocol = 'symphony://',
TelProtocol = 'tel:', TelProtocol = 'tel:',
SmsProtocol = 'sms:',
} }
class ProtocolHandler { class ProtocolHandler {
@ -87,11 +88,8 @@ class ProtocolHandler {
`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); this.preloadWebContents.send('protocol-action', url);
} else if (url?.includes('tel:')) { } else if (url?.includes('tel:') || url?.includes('sms:')) {
this.preloadWebContents.send( this.preloadWebContents.send('phone-number-received', url);
'phone-number-received',
url.split('tel:')[1],
);
} }
} }
@ -112,6 +110,11 @@ class ProtocolHandler {
protocol.TelProtocol, protocol.TelProtocol,
false, false,
); );
const smsArgFromArgv = getCommandLineArgs(
argv || process.argv,
protocol.SmsProtocol,
false,
);
if (protocolUriFromArgv) { if (protocolUriFromArgv) {
logger.info( logger.info(
`protocol-handler: we have a protocol request for the url ${protocolUriFromArgv}!`, `protocol-handler: we have a protocol request for the url ${protocolUriFromArgv}!`,
@ -122,6 +125,11 @@ class ProtocolHandler {
`protocol-handler: we have a tel request for ${telArgFromArgv}!`, `protocol-handler: we have a tel request for ${telArgFromArgv}!`,
); );
this.sendProtocol(telArgFromArgv, isAppAlreadyOpen); this.sendProtocol(telArgFromArgv, isAppAlreadyOpen);
} else if (smsArgFromArgv) {
logger.info(
`protocol-handler: we have an sms request for ${smsArgFromArgv}!`,
);
this.sendProtocol(smsArgFromArgv, isAppAlreadyOpen);
} }
} }

View File

@ -2,43 +2,49 @@ import { exec } from 'child_process';
import { app } from 'electron'; import { app } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { PhoneNumberProtocol } from '../common/api-interface';
import { isDevEnv, isMac, isWindowsOS } from '../common/env'; import { isDevEnv, isMac, isWindowsOS } from '../common/env';
import { logger } from '../common/logger'; import { logger } from '../common/logger';
const LS_REGISTER_PATH = const LS_REGISTER_PATH =
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister'; '/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 { enum REGISTRY_PATHS {
Classes = '\\Software\\Classes',
Capabilities = '\\Software\\Symphony\\Capabilities', Capabilities = '\\Software\\Symphony\\Capabilities',
UrlRegistration = '\\Software\\Symphony\\Capabilities\\URLAssociations', UrlRegistration = '\\Software\\Symphony\\Capabilities\\URLAssociations',
SymTelCmd = '\\Software\\Classes\\Symphony.tel\\shell\\open\\command', SymTelCmd = '\\Software\\Classes\\Symphony.tel\\shell\\open\\command',
SymSmsCmd = '\\Software\\Classes\\Symphony.sms\\shell\\open\\command',
SymTelDefaultIcon = '\\Software\\Classes\\Symphony.tel\\DefaultIcon', SymTelDefaultIcon = '\\Software\\Classes\\Symphony.tel\\DefaultIcon',
SymSmsDefaultIcon = '\\Software\\Classes\\Symphony.sms\\DefaultIcon',
RegisteredApps = '\\Software\\RegisteredApplications', RegisteredApps = '\\Software\\RegisteredApplications',
} }
class VoiceHandler { class VoiceHandler {
/** /**
* Registers Symphony as phone calls app * Registers Symphony as phone calls/SMS app
*/ */
public registerSymphonyAsDefaultCallApp() { public registerSymphonyAsDefaultApp(protocols: PhoneNumberProtocol[]) {
if (isWindowsOS) { if (isWindowsOS) {
this.registerAppOnWindows(); this.registerAppOnWindows(protocols);
} else if (isMac) { } else if (isMac) {
this.registerAppOnMacOS(); this.registerAppOnMacOS(protocols);
}
}
/**
* Unregisters Symphony as phone calls/SMS app
*/
public unregisterSymphonyAsDefaultApp(protocols: PhoneNumberProtocol[]) {
if (isWindowsOS) {
this.unregisterAppOnWindows(protocols);
} else if (isMac) {
this.unregisterAppOnMacOS(protocols);
} }
} }
/** /**
* Registers app on Windows * Registers app on Windows
*/ */
private async registerAppOnWindows() { private async registerAppOnWindows(protocols: PhoneNumberProtocol[]) {
const Registry = require('winreg'); const Registry = require('winreg');
const appPath = isDevEnv const appPath = isDevEnv
? path.join(path.dirname(app.getPath('exe')), 'Electron.exe') ? path.join(path.dirname(app.getPath('exe')), 'Electron.exe')
@ -51,18 +57,21 @@ class VoiceHandler {
hive: Registry.HKCU, hive: Registry.HKCU,
key: REGISTRY_PATHS.UrlRegistration, 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({ const symAppRegistrationRegKey = new Registry({
hive: Registry.HKCU, hive: Registry.HKCU,
key: REGISTRY_PATHS.RegisteredApps, key: REGISTRY_PATHS.RegisteredApps,
}); });
for (const protocol of protocols) {
const keys = this.getProtocolSpecificKeys(protocol);
const symCommandRegKey = new Registry({
hive: Registry.HKCU,
key: keys.symCommandKey,
});
const symDefaultIconRegKey = new Registry({
hive: Registry.HKCU,
key: keys.symDefaultIconKey,
});
const errorCallback = (error) => { const errorCallback = (error) => {
if (error) { if (error) {
logger.error( logger.error(
@ -84,18 +93,18 @@ class VoiceHandler {
errorCallback, errorCallback,
); );
await symURLAssociationRegKey.set( await symURLAssociationRegKey.set(
'tel', protocol,
Registry.REG_SZ, Registry.REG_SZ,
'Symphony.tel', `Symphony.${protocol}`,
errorCallback, errorCallback,
); );
await symTelDefaultIconRegKey.set( await symDefaultIconRegKey.set(
'', '',
Registry.REG_SZ, Registry.REG_SZ,
appPath, appPath,
errorCallback, errorCallback,
); );
await symTelCommandRegKey.set( await symCommandRegKey.set(
'', '',
Registry.REG_SZ, Registry.REG_SZ,
`"${appPath}" "%1"`, `"${appPath}" "%1"`,
@ -108,22 +117,106 @@ class VoiceHandler {
errorCallback, errorCallback,
); );
} }
}
/**
* Returns Windows call/sms specific registry keys calls/SMS
*/
private getProtocolSpecificKeys(protocol) {
const keys = {
symCommandKey: '',
symDefaultIconKey: '',
};
switch (protocol) {
case PhoneNumberProtocol.Sms:
keys.symCommandKey = REGISTRY_PATHS.SymSmsCmd;
keys.symDefaultIconKey = REGISTRY_PATHS.SymSmsDefaultIcon;
break;
case PhoneNumberProtocol.Tel:
keys.symCommandKey = REGISTRY_PATHS.SymTelCmd;
keys.symDefaultIconKey = REGISTRY_PATHS.SymTelDefaultIcon;
break;
default:
logger.info('voice-handler: unsupported protocol');
break;
}
return keys;
}
/**
* Unregisters tel / sms protocols on Windows
*/
private async unregisterAppOnWindows(protocols: PhoneNumberProtocol[]) {
const Registry = require('winreg');
const symURLAssociationRegKey = new Registry({
hive: Registry.HKCU,
key: REGISTRY_PATHS.UrlRegistration,
});
const errorCallback = (error) => {
if (error) {
logger.error(
'voice-handler: error while removing voice registry keys: ',
error,
);
}
};
const DestroyErrorCallback = (error) => {
if (error) {
logger.error(
'voice-handler: error while destroying voice registry keys: ',
error,
);
}
};
for (const protocol of protocols) {
await symURLAssociationRegKey.remove(protocol, errorCallback);
const symprotocolClassRegKey = new Registry({
hive: Registry.HKCU,
key: `${REGISTRY_PATHS.Classes}\\Symphony.${protocol}`,
});
await symprotocolClassRegKey.destroy(DestroyErrorCallback);
}
}
/** /**
* Registers app on macOS * Registers app on macOS
*/ */
private registerAppOnMacOS() { private registerAppOnMacOS(protocols: PhoneNumberProtocol[]) {
this.readLaunchServicesPlist((res, _err) => { this.readLaunchServicesPlist((plist) => {
const data = res; for (const protocol of protocols) {
const itemIdx = data.findIndex( const itemIdx = plist.LSHandlers.findIndex(
(item) => item.LSHandlerURLScheme === DEFAULT_TEL_PROTOCOL, (lsHandler) => lsHandler.LSHandlerURLScheme === protocol,
); );
// macOS allows only one app being declared as able to make calls // macOS allows only one app being declared as able to make calls
if (itemIdx !== -1) { if (itemIdx !== -1) {
data.splice(itemIdx, 1); plist.splice(itemIdx, 1);
}
const plistEntry = {
LSHandlerURLScheme: protocol,
LSHandlerRoleAll: 'com.symphony.electron-desktop',
LSHandlerPreferredVersions: {
LSHandlerRoleAll: '-',
},
};
plist.LSHandlers.push(plistEntry);
}
this.updateLaunchServicesPlist(plist);
});
}
/**
* Unregisters app for tel/sms on macOS
*/
private unregisterAppOnMacOS(protocols: PhoneNumberProtocol[]) {
this.readLaunchServicesPlist((plist) => {
if (plist) {
const filteredList = plist.LSHandlers.filter(
(lsHandler) => protocols.indexOf(lsHandler.LSHandlerURLScheme) === -1,
);
plist.LSHandlers = filteredList;
this.updateLaunchServicesPlist(plist);
} }
data.push(SYM_TEL_PROTOCOL_PLIST_ENTRY);
this.updateLaunchServicesPlist(data);
}); });
} }
@ -136,22 +229,28 @@ class VoiceHandler {
const tmpPath = `${plistPath}.${Math.random()}`; const tmpPath = `${plistPath}.${Math.random()}`;
exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, (err) => { exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, (err) => {
if (err) { if (err) {
callback(err); logger.error(
'voice-handler: error while converting binary file: ',
err,
);
return; return;
} }
fs.readFile(tmpPath, (readErr, data) => { fs.readFile(tmpPath, (readErr, data) => {
if (readErr) { if (readErr) {
callback(readErr); logger.error('voice-handler: error while reading tmp file:');
return; return;
} }
try { try {
const json = JSON.parse(data.toString()); const plistContent = JSON.parse(data.toString());
callback(json.LSHandlers, json); callback(plistContent);
fs.unlink(tmpPath, (err) => { fs.unlink(tmpPath, (err) => {
logger.error('Error: ', err); if (err) {
logger.error('voice-handler: error clearing tmp file ', err);
}
}); });
} catch (e) { } catch (e) {
callback(e); logger.error('voice-handler: unexpected error occured ', err);
return;
} }
}); });
}); });
@ -164,24 +263,17 @@ class VoiceHandler {
private updateLaunchServicesPlist(defaults) { private updateLaunchServicesPlist(defaults) {
const plistPath = this.getLaunchServicesPlistPath(); const plistPath = this.getLaunchServicesPlistPath();
const tmpPath = `${plistPath}.${Math.random()}`; 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 { try {
let data = fs.readFileSync(tmpPath).toString(); fs.writeFileSync(tmpPath, JSON.stringify(defaults));
data = JSON.parse(data);
(data as any).LSHandlers = defaults;
data = JSON.stringify(data);
fs.writeFileSync(tmpPath, data);
} catch (e) { } catch (e) {
logger.error('voice-handler: error while converting plist ', err); logger.error('voice-handler: error while creating tmp plist ', e);
return; return;
} }
exec(`plutil -convert binary1 "${tmpPath}" -o "${plistPath}"`, () => { exec(`plutil -convert binary1 "${tmpPath}" -o "${plistPath}"`, () => {
fs.unlink(tmpPath, (err) => { fs.unlink(tmpPath, (err) => {
if (err) {
logger.error(`voice-handler: error while clearing ${tmpPath}: `, err); logger.error(`voice-handler: error while clearing ${tmpPath}: `, err);
}
}); });
// Relaunch Launch Services so it take into consideration updated plist file // Relaunch Launch Services so it take into consideration updated plist file
exec( exec(
@ -196,7 +288,6 @@ class VoiceHandler {
}, },
); );
}); });
});
} }
/** /**

View File

@ -76,7 +76,8 @@ export enum apiCmds {
updateMyPresence = 'update-my-presence', updateMyPresence = 'update-my-presence',
getMyPresence = 'get-my-presence', getMyPresence = 'get-my-presence',
updateSymphonyTray = 'update-system-tray', updateSymphonyTray = 'update-system-tray',
registerVoiceServices = 'register-voice-services', registerPhoneNumberServices = 'register-phone-numbers-services',
unregisterPhoneNumberServices = 'unregister-phone-numbers-services',
} }
export enum apiName { export enum apiName {
@ -137,6 +138,7 @@ export interface IApiArgs {
autoUpdateTrigger: AutoUpdateTrigger; autoUpdateTrigger: AutoUpdateTrigger;
hideOnCapture: boolean; hideOnCapture: boolean;
status: IPresenceStatus; status: IPresenceStatus;
protocols: PhoneNumberProtocol[];
} }
export type Themes = 'light' | 'dark'; export type Themes = 'light' | 'dark';
@ -376,3 +378,8 @@ export interface IAuthResponse {
ssoDisabledForMobile: boolean; ssoDisabledForMobile: boolean;
keymanagerUrl: string; keymanagerUrl: string;
} }
export enum PhoneNumberProtocol {
Tel = 'tel',
Sms = 'sms',
}

View File

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

View File

@ -30,6 +30,7 @@ import {
KeyCodes, KeyCodes,
LogLevel, LogLevel,
NotificationActionCallback, NotificationActionCallback,
PhoneNumberProtocol,
} from '../common/api-interface'; } from '../common/api-interface';
import { i18n, LocaleType } from '../common/i18n-preload'; import { i18n, LocaleType } from '../common/i18n-preload';
import { throttle } from '../common/utils'; import { throttle } from '../common/utils';
@ -879,17 +880,30 @@ export class SSFApi {
} }
/** /**
* Allows JS to register SDA for calls * Allows JS to register SDA for phone numbers clicks
* @param {Function} phoneNumberCallback callback function invoked when receiving a phone number * @param {Function} phoneNumberCallback callback function invoked when receiving a phone number for calls/sms
*/ */
public registerVoiceServices( public registerPhoneNumberServices(
protocols: PhoneNumberProtocol[],
phoneNumberCallback: (arg: string) => void, phoneNumberCallback: (arg: string) => void,
): void { ): void {
if (typeof phoneNumberCallback === 'function') { if (typeof phoneNumberCallback === 'function') {
local.phoneNumberCallback = phoneNumberCallback; local.phoneNumberCallback = phoneNumberCallback;
} }
ipcRenderer.send(apiName.symphonyApi, { ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.registerVoiceServices, cmd: apiCmds.registerPhoneNumberServices,
protocols,
});
}
/**
* Allows JS to unregister app to sms/call protocols
* @param protocol protocol to be unregistered.
*/
public unregisterPhoneNumberServices(protocols: PhoneNumberProtocol[]) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.unregisterPhoneNumberServices,
protocols,
}); });
} }
} }