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.tel", false);
key.DeleteSubKeyTree("Symphony.sms", false);
}
}

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import { windowHandler } from './window-handler';
enum protocol {
SymphonyProtocol = 'symphony://',
TelProtocol = 'tel:',
SmsProtocol = 'sms:',
}
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!`,
);
this.preloadWebContents.send('protocol-action', url);
} else if (url?.includes('tel:')) {
this.preloadWebContents.send(
'phone-number-received',
url.split('tel:')[1],
);
} else if (url?.includes('tel:') || url?.includes('sms:')) {
this.preloadWebContents.send('phone-number-received', url);
}
}
@ -112,6 +110,11 @@ class ProtocolHandler {
protocol.TelProtocol,
false,
);
const smsArgFromArgv = getCommandLineArgs(
argv || process.argv,
protocol.SmsProtocol,
false,
);
if (protocolUriFromArgv) {
logger.info(
`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}!`,
);
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 * as fs from 'fs';
import * as path from 'path';
import { PhoneNumberProtocol } from '../common/api-interface';
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 {
Classes = '\\Software\\Classes',
Capabilities = '\\Software\\Symphony\\Capabilities',
UrlRegistration = '\\Software\\Symphony\\Capabilities\\URLAssociations',
SymTelCmd = '\\Software\\Classes\\Symphony.tel\\shell\\open\\command',
SymSmsCmd = '\\Software\\Classes\\Symphony.sms\\shell\\open\\command',
SymTelDefaultIcon = '\\Software\\Classes\\Symphony.tel\\DefaultIcon',
SymSmsDefaultIcon = '\\Software\\Classes\\Symphony.sms\\DefaultIcon',
RegisteredApps = '\\Software\\RegisteredApplications',
}
class VoiceHandler {
/**
* Registers Symphony as phone calls app
* Registers Symphony as phone calls/SMS app
*/
public registerSymphonyAsDefaultCallApp() {
public registerSymphonyAsDefaultApp(protocols: PhoneNumberProtocol[]) {
if (isWindowsOS) {
this.registerAppOnWindows();
this.registerAppOnWindows(protocols);
} 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
*/
private async registerAppOnWindows() {
private async registerAppOnWindows(protocols: PhoneNumberProtocol[]) {
const Registry = require('winreg');
const appPath = isDevEnv
? path.join(path.dirname(app.getPath('exe')), 'Electron.exe')
@ -51,79 +57,166 @@ class VoiceHandler {
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,
});
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) => {
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(
protocol,
Registry.REG_SZ,
`Symphony.${protocol}`,
errorCallback,
);
await symDefaultIconRegKey.set(
'',
Registry.REG_SZ,
appPath,
errorCallback,
);
await symCommandRegKey.set(
'',
Registry.REG_SZ,
`"${appPath}" "%1"`,
errorCallback,
);
await symAppRegistrationRegKey.set(
'Symphony',
Registry.REG_SZ,
'Software\\Symphony\\Capabilities',
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 creating voice registry keys: ',
'voice-handler: error while removing 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,
);
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
*/
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);
private registerAppOnMacOS(protocols: PhoneNumberProtocol[]) {
this.readLaunchServicesPlist((plist) => {
for (const protocol of protocols) {
const itemIdx = plist.LSHandlers.findIndex(
(lsHandler) => lsHandler.LSHandlerURLScheme === protocol,
);
// macOS allows only one app being declared as able to make calls
if (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()}`;
exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, (err) => {
if (err) {
callback(err);
logger.error(
'voice-handler: error while converting binary file: ',
err,
);
return;
}
fs.readFile(tmpPath, (readErr, data) => {
if (readErr) {
callback(readErr);
logger.error('voice-handler: error while reading tmp file:');
return;
}
try {
const json = JSON.parse(data.toString());
callback(json.LSHandlers, json);
const plistContent = JSON.parse(data.toString());
callback(plistContent);
fs.unlink(tmpPath, (err) => {
logger.error('Error: ', err);
if (err) {
logger.error('voice-handler: error clearing tmp file ', err);
}
});
} catch (e) {
callback(e);
logger.error('voice-handler: unexpected error occured ', err);
return;
}
});
});
@ -164,38 +263,30 @@ class VoiceHandler {
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) => {
try {
fs.writeFileSync(tmpPath, JSON.stringify(defaults));
} catch (e) {
logger.error('voice-handler: error while creating tmp plist ', e);
return;
}
exec(`plutil -convert binary1 "${tmpPath}" -o "${plistPath}"`, () => {
fs.unlink(tmpPath, (err) => {
if (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,
);
}
},
);
}
});
// 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,
);
}
},
);
});
}

View File

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

View File

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

View File

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