diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3a34c87ef1..f23410bff9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3430,7 +3430,12 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); QString addressLookupString; if (urlIndex != -1) { - addressLookupString = arguments().value(urlIndex + 1); + QUrl url(arguments().value(urlIndex + 1)); + if (url.scheme() == URL_SCHEME_HIFIAPP) { + Setting::Handle("startUpApp").set(url.path()); + } else { + addressLookupString = url.toString(); + } } static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; @@ -7678,6 +7683,9 @@ void Application::openUrl(const QUrl& url) const { if (!url.isEmpty()) { if (url.scheme() == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); + } else if (url.scheme() == URL_SCHEME_HIFIAPP) { + QmlCommerce commerce; + commerce.openSystemApp(url.path()); } else { // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 7c5df0f3e3..aa39fdc1b9 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -47,6 +47,54 @@ QmlCommerce::QmlCommerce() { _appsPath = PathUtils::getAppDataPath() + "Apps/"; } + + + +void QmlCommerce::openSystemApp(const QString& appName) { + static QMap systemApps { + {"GOTO", "hifi/tablet/TabletAddressDialog.qml"}, + {"PEOPLE", "hifi/Pal.qml"}, + {"WALLET", "hifi/commerce/wallet/Wallet.qml"}, + {"MARKET", "/marketplace.html"} + }; + + static QMap systemInject{ + {"MARKET", "/scripts/system/html/js/marketplacesInject.js"} + }; + + + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + + QMap::const_iterator appPathIter = systemApps.find(appName); + if (appPathIter != systemApps.end()) { + if (appPathIter->contains(".qml", Qt::CaseInsensitive)) { + tablet->loadQMLSource(*appPathIter); + } + else if (appPathIter->contains(".html", Qt::CaseInsensitive)) { + QMap::const_iterator injectIter = systemInject.find(appName); + if (appPathIter == systemInject.end()) { + tablet->gotoWebScreen(NetworkingConstants::METAVERSE_SERVER_URL().toString() + *appPathIter); + } + else { + QString inject = "file:///" + qApp->applicationDirPath() + *injectIter; + tablet->gotoWebScreen(NetworkingConstants::METAVERSE_SERVER_URL().toString() + *appPathIter, inject); + } + } + else { + qCDebug(commerce) << "Attempted to open unknown type of URL!"; + return; + } + } + else { + qCDebug(commerce) << "Attempted to open unknown APP!"; + return; + } + + DependencyManager::get()->openTablet(); +} + + void QmlCommerce::getWalletStatus() { auto wallet = DependencyManager::get(); wallet->getWalletStatus(); @@ -360,7 +408,7 @@ bool QmlCommerce::openApp(const QString& itemHref) { // Read from the file to know what .html or .qml document to open QFile appFile(_appsPath + "/" + appHref.fileName()); if (!appFile.open(QIODevice::ReadOnly)) { - qCDebug(commerce) << "Couldn't open local .app.json file."; + qCDebug(commerce) << "Couldn't open local .app.json file:" << appFile; return false; } QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 79d8e82e71..bee30e1b62 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -24,6 +24,7 @@ class QmlCommerce : public QObject { public: QmlCommerce(); + void openSystemApp(const QString& appPath); signals: void walletStatusResult(uint walletStatus); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 3e3c9da148..d9396ae4d1 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -176,7 +176,7 @@ int main(int argc, const char* argv[]) { if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) { if (parser.isSet(urlOption)) { QUrl url = QUrl(parser.value(urlOption)); - if (url.isValid() && url.scheme() == URL_SCHEME_HIFI) { + if (url.isValid() && (url.scheme() == URL_SCHEME_HIFI || url.scheme() == URL_SCHEME_HIFIAPP)) { qDebug() << "Writing URL to local socket"; socket.write(url.toString().toUtf8()); if (!socket.waitForBytesWritten(5000)) { diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 31ff6da873..839e269fd4 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -32,6 +32,7 @@ namespace NetworkingConstants { const QString URL_SCHEME_ABOUT = "about"; const QString URL_SCHEME_HIFI = "hifi"; +const QString URL_SCHEME_HIFIAPP = "hifiapp"; const QString URL_SCHEME_QRC = "qrc"; const QString URL_SCHEME_FILE = "file"; const QString URL_SCHEME_HTTP = "http"; diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 12ba115815..83d99cd42b 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -334,5 +334,11 @@ function AppUi(properties) { Script.scriptEnding.connect(that.onScriptEnding); GlobalServices.findableByChanged.connect(restartNotificationPoll); GlobalServices.myUsernameChanged.connect(restartNotificationPoll); + if (that.buttonName == Settings.getValue("startUpApp")) { + Settings.setValue("startUpApp", ""); + Script.setTimeout(function () { + that.open(); + }, 1000); + } } module.exports = AppUi; diff --git a/server-console/resources/tray-menu-notification.png b/server-console/resources/tray-menu-notification.png new file mode 100644 index 0000000000..0d6e15752f Binary files /dev/null and b/server-console/resources/tray-menu-notification.png differ diff --git a/server-console/src/main.js b/server-console/src/main.js index 92ebdbf36c..95b5935255 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -29,92 +29,44 @@ const updater = require('./modules/hf-updater.js'); const Config = require('./modules/config').Config; const hfprocess = require('./modules/hf-process.js'); + +global.log = require('electron-log'); + const Process = hfprocess.Process; const ACMonitorProcess = hfprocess.ACMonitorProcess; const ProcessStates = hfprocess.ProcessStates; const ProcessGroup = hfprocess.ProcessGroup; const ProcessGroupStates = hfprocess.ProcessGroupStates; +const hfApp = require('./modules/hf-app.js'); +const GetBuildInfo = hfApp.getBuildInfo; +const StartInterface = hfApp.startInterface; +const getRootHifiDataDirectory = hfApp.getRootHifiDataDirectory; +const getDomainServerClientResourcesDirectory = hfApp.getDomainServerClientResourcesDirectory; +const getAssignmentClientResourcesDirectory = hfApp.getAssignmentClientResourcesDirectory; +const getApplicationDataDirectory = hfApp.getApplicationDataDirectory; + + const osType = os.type(); const appIcon = path.join(__dirname, '../resources/console.png'); +const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notification.png'); + const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; -function getBuildInfo() { - var buildInfoPath = null; +const buildInfo = GetBuildInfo(); - if (osType == 'Windows_NT') { - buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); - } else if (osType == 'Darwin') { - var contentPath = ".app/Contents/"; - var contentEndIndex = __dirname.indexOf(contentPath); - if (contentEndIndex != -1) { - // this is an app bundle - var appPath = __dirname.substring(0, contentEndIndex) + ".app"; - buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); - } - } - - const DEFAULT_BUILD_INFO = { - releaseType: "", - buildIdentifier: "dev", - buildNumber: "0", - stableBuild: "0", - organization: "High Fidelity - dev", - appUserModelId: "com.highfidelity.sandbox-dev" - }; - var buildInfo = DEFAULT_BUILD_INFO; - - if (buildInfoPath) { - try { - buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); - } catch (e) { - buildInfo = DEFAULT_BUILD_INFO; - } - } - - return buildInfo; -} -const buildInfo = getBuildInfo(); - -function getRootHifiDataDirectory(local) { - var organization = buildInfo.organization; - if (osType == 'Windows_NT') { - if (local) { - return path.resolve(osHomeDir(), 'AppData/Local', organization); - } else { - return path.resolve(osHomeDir(), 'AppData/Roaming', organization); - } - } else if (osType == 'Darwin') { - return path.resolve(osHomeDir(), 'Library/Application Support', organization); - } else { - return path.resolve(osHomeDir(), '.local/share/', organization); - } -} - -function getDomainServerClientResourcesDirectory() { - return path.join(getRootHifiDataDirectory(), '/domain-server'); -} - -function getAssignmentClientResourcesDirectory() { - return path.join(getRootHifiDataDirectory(), '/assignment-client'); -} - -function getApplicationDataDirectory(local) { - return path.join(getRootHifiDataDirectory(local), '/Server Console'); -} // Update lock filepath const UPDATER_LOCK_FILENAME = ".updating"; const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_FILENAME; // Configure log -global.log = require('electron-log'); const oldLogFile = path.join(getApplicationDataDirectory(), '/log.txt'); const logFile = path.join(getApplicationDataDirectory(true), '/log.txt'); if (oldLogFile != logFile && fs.existsSync(oldLogFile)) { @@ -149,15 +101,23 @@ const configPath = path.join(getApplicationDataDirectory(), 'config.json'); var userConfig = new Config(); userConfig.load(configPath); - const ipcMain = electron.ipcMain; + +function isServerInstalled() { + return interfacePath && userConfig.get("serverInstalled", true); +} + +function isInterfaceInstalled() { + return dsPath && acPath && userConfig.get("interfaceInstalled", true); +} + var isShuttingDown = false; function shutdown() { log.debug("Normal shutdown (isShuttingDown: " + isShuttingDown + ")"); if (!isShuttingDown) { // if the home server is running, show a prompt before quit to ask if the user is sure - if (homeServer.state == ProcessGroupStates.STARTED) { + if (isServerInstalled() && homeServer.state == ProcessGroupStates.STARTED) { log.debug("Showing shutdown dialog."); dialog.showMessageBox({ type: 'question', @@ -184,6 +144,9 @@ function shutdownCallback(idx) { if (idx == 0 && !isShuttingDown) { isShuttingDown = true; + log.debug("Stop tray polling."); + trayNotifications.stopPolling(); + log.debug("Saving user config"); userConfig.save(configPath); @@ -191,31 +154,37 @@ function shutdownCallback(idx) { log.debug("Closing log window"); logWindow.close(); } - if (homeServer) { - log.debug("Stoping home server"); - homeServer.stop(); - } - updateTrayMenu(homeServer.state); + if (isServerInstalled()) { + if (homeServer) { + log.debug("Stoping home server"); + homeServer.stop(); - if (homeServer.state == ProcessGroupStates.STOPPED) { - // if the home server is already down, take down the server console now - log.debug("Quitting."); - app.exit(0); - } else { - // if the home server is still running, wait until we get a state change or timeout - // before quitting the app - log.debug("Server still shutting down. Waiting"); - var timeoutID = setTimeout(function() { - app.exit(0); - }, 5000); - homeServer.on('state-update', function(processGroup) { - if (processGroup.state == ProcessGroupStates.STOPPED) { - clearTimeout(timeoutID); + updateTrayMenu(homeServer.state); + + if (homeServer.state == ProcessGroupStates.STOPPED) { + // if the home server is already down, take down the server console now log.debug("Quitting."); app.exit(0); + } else { + // if the home server is still running, wait until we get a state change or timeout + // before quitting the app + log.debug("Server still shutting down. Waiting"); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); + homeServer.on('state-update', function(processGroup) { + if (processGroup.state == ProcessGroupStates.STOPPED) { + clearTimeout(timeoutID); + log.debug("Quitting."); + app.exit(0); + } + }); } - }); + } + } + else { + app.exit(0); } } } @@ -351,20 +320,6 @@ function openLogDirectory() { app.on('window-all-closed', function() { }); -function startInterface(url) { - var argArray = []; - - // check if we have a url parameter to include - if (url) { - argArray = ["--url", url]; - } - - // create a new Interface instance - Interface makes sure only one is running at a time - var pInterface = new Process('interface', interfacePath, argArray); - pInterface.detached = true; - pInterface.start(); -} - var tray = null; global.homeServer = null; global.domainServer = null; @@ -372,6 +327,18 @@ global.acMonitor = null; global.userConfig = userConfig; global.openLogDirectory = openLogDirectory; +const hfNotifications = require('./modules/hf-notifications.js'); +const HifiNotifications = hfNotifications.HifiNotifications; +const HifiNotificationType = hfNotifications.NotificationType; + +var pendingNotifications = {} +function notificationCallback(notificationType, pending = true) { + pendingNotifications[notificationType] = pending; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); +} + +var trayNotifications = new HifiNotifications(userConfig, notificationCallback); + var LogWindow = function(ac, ds) { this.ac = ac; this.ds = ds; @@ -407,7 +374,7 @@ LogWindow.prototype = { function visitSandboxClicked() { if (interfacePath) { - startInterface('hifi://localhost'); + StartInterface('hifi://localhost'); } else { // show an error to say that we can't go home without an interface instance dialog.showErrorBox("Client Not Found", binaryMissingMessage("High Fidelity client", "Interface", false)); @@ -425,6 +392,48 @@ var labels = { label: 'Version - ' + buildInfo.buildIdentifier, enabled: false }, + showNotifications: { + label: 'Show Notifications', + type: 'checkbox', + checked: true, + click: function () { + trayNotifications.enable(!trayNotifications.enabled(), notificationCallback); + userConfig.save(configPath); + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + goto: { + label: 'GoTo', + click: function () { + StartInterface("hifiapp:GOTO"); + pendingNotifications[HifiNotificationType.GOTO] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + people: { + label: 'People', + click: function () { + StartInterface("hifiapp:PEOPLE"); + pendingNotifications[HifiNotificationType.PEOPLE] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + wallet: { + label: 'Wallet', + click: function () { + StartInterface("hifiapp:WALLET"); + pendingNotifications[HifiNotificationType.WALLET] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + marketplace: { + label: 'Market', + click: function () { + StartInterface("hifiapp:MARKET"); + pendingNotifications[HifiNotificationType.MARKETPLACE] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, restart: { label: 'Start Server', click: function() { @@ -489,22 +498,36 @@ function buildMenuArray(serverState) { if (isShuttingDown) { menuArray.push(labels.shuttingDown); } else { - menuArray.push(labels.serverState); - menuArray.push(labels.version); - menuArray.push(separator); - menuArray.push(labels.goHome); - menuArray.push(separator); - menuArray.push(labels.restart); - menuArray.push(labels.stopServer); - menuArray.push(labels.settings); - menuArray.push(labels.viewLogs); - menuArray.push(separator); + if (isServerInstalled()) { + menuArray.push(labels.serverState); + menuArray.push(labels.version); + menuArray.push(separator); + } + if (isServerInstalled() && isInterfaceInstalled()) { + menuArray.push(labels.goHome); + menuArray.push(separator); + } + if (isServerInstalled()) { + menuArray.push(labels.restart); + menuArray.push(labels.stopServer); + menuArray.push(labels.settings); + menuArray.push(labels.viewLogs); + menuArray.push(separator); + } menuArray.push(labels.share); menuArray.push(separator); + if (isInterfaceInstalled()) { + menuArray.push(labels.goto); + menuArray.push(labels.people); + menuArray.push(labels.wallet); + menuArray.push(labels.marketplace); + menuArray.push(separator); + menuArray.push(labels.showNotifications); + menuArray.push(separator); + } menuArray.push(labels.quit); } - return menuArray; } @@ -528,6 +551,17 @@ function updateLabels(serverState) { labels.restart.label = "Restart Server"; labels.restart.enabled = false; } + + labels.showNotifications.checked = trayNotifications.enabled(); + labels.people.visible = trayNotifications.enabled(); + labels.goto.visible = trayNotifications.enabled(); + labels.wallet.visible = trayNotifications.enabled(); + labels.marketplace.visible = trayNotifications.enabled(); + labels.goto.icon = pendingNotifications[HifiNotificationType.GOTO] ? menuNotificationIcon : null; + labels.people.icon = pendingNotifications[HifiNotificationType.PEOPLE] ? menuNotificationIcon : null; + labels.wallet.icon = pendingNotifications[HifiNotificationType.WALLET] ? menuNotificationIcon : null; + labels.marketplace.icon = pendingNotifications[HifiNotificationType.MARKETPLACE] ? menuNotificationIcon : null; + } function updateTrayMenu(serverState) { @@ -807,7 +841,7 @@ function onContentLoaded() { deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); - if (dsPath && acPath) { + if (isServerInstalled()) { var dsArguments = ['--get-temp-name', '--parent-pid', process.pid]; domainServer = new Process('domain-server', dsPath, dsArguments, logPath); @@ -838,7 +872,7 @@ function onContentLoaded() { // If we were launched with the launchInterface option, then we need to launch interface now if (argv.launchInterface) { log.debug("Interface launch requested... argv.launchInterface:", argv.launchInterface); - startInterface(); + StartInterface(); } // If we were launched with the shutdownWith option, then we need to shutdown when that process (pid) @@ -869,7 +903,7 @@ app.on('ready', function() { // Create tray icon tray = new Tray(trayIcons[ProcessGroupStates.STOPPED]); - tray.setToolTip('High Fidelity Sandbox'); + tray.setToolTip('High Fidelity'); tray.on('click', function() { tray.popUpContextMenu(tray.menu); diff --git a/server-console/src/modules/hf-acctinfo.js b/server-console/src/modules/hf-acctinfo.js new file mode 100644 index 0000000000..828bc781b8 --- /dev/null +++ b/server-console/src/modules/hf-acctinfo.js @@ -0,0 +1,138 @@ +'use strict' + +const request = require('request'); +const extend = require('extend'); +const util = require('util'); +const events = require('events'); +const childProcess = require('child_process'); +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); + +const hfApp = require('./hf-app.js'); +const getInterfaceDataDirectory = hfApp.getInterfaceDataDirectory; + + +const VariantTypes = { + USER_TYPE: 1024 +} + +function AccountInfo() { + + var accountInfoPath = path.join(getInterfaceDataDirectory(), '/AccountInfo.bin'); + this.rawData = null; + this.parseOffset = 0; + try { + this.rawData = fs.readFileSync(accountInfoPath); + + this.data = this._parseMap(); + + } catch(e) { + console.log(e); + log.debug("AccountInfo file not found: " + accountInfoPath); + } +} + +AccountInfo.prototype = { + + accessToken: function (metaverseUrl) { + if (this.data && this.data[metaverseUrl] && this.data[metaverseUrl]["accessToken"]) { + return this.data[metaverseUrl]["accessToken"]["token"]; + } + return null; + }, + _parseUInt32: function () { + if (!this.rawData || (this.rawData.length - this.parseOffset < 4)) { + throw "Expected uint32"; + } + var result = this.rawData.readUInt32BE(this.parseOffset); + this.parseOffset += 4; + return result; + }, + _parseMap: function () { + var result = {}; + var n = this._parseUInt32(); + for (var i = 0; i < n; i++) { + var key = this._parseQString(); + result[key] = this._parseVariant(); + } + return result; + }, + _parseVariant: function () { + var varType = this._parseUInt32(); + var isNull = this.rawData[this.parseOffset++]; + + switch (varType) { + case VariantTypes.USER_TYPE: + //user type + var userTypeName = this._parseByteArray().toString('ascii').slice(0,-1); + if (userTypeName == "DataServerAccountInfo") { + return this._parseDataServerAccountInfo(); + } + else { + throw "Unknown custom type " + userTypeName; + } + break; + } + }, + _parseByteArray: function () { + var length = this._parseUInt32(); + if (length == 0xffffffff) { + return null; + } + var result = this.rawData.slice(this.parseOffset, this.parseOffset+length); + this.parseOffset += length; + return result; + + }, + _parseQString: function () { + if (!this.rawData || (this.rawData.length <= this.parseOffset)) { + throw "Expected QString"; + } + // length in bytes; + var length = this._parseUInt32(); + if (length == 0xFFFFFFFF) { + return null; + } + + if (this.rawData.length - this.parseOffset < length) { + throw "Insufficient buffer length for QString parsing"; + } + + // Convert from BE UTF16 to LE + var resultBuffer = this.rawData.slice(this.parseOffset, this.parseOffset+length); + resultBuffer.swap16(); + var result = resultBuffer.toString('utf16le'); + this.parseOffset += length; + return result; + }, + _parseDataServerAccountInfo: function () { + return { + accessToken: this._parseOAuthAccessToken(), + username: this._parseQString(), + xmppPassword: this._parseQString(), + discourseApiKey: this._parseQString(), + walletId: this._parseUUID(), + privateKey: this._parseByteArray(), + domainId: this._parseUUID(), + tempDomainId: this._parseUUID(), + tempDomainApiKey: this._parseQString() + + } + }, + _parseOAuthAccessToken: function () { + return { + token: this._parseQString(), + timestampHigh: this._parseUInt32(), + timestampLow: this._parseUInt32(), + tokenType: this._parseQString(), + refreshToken: this._parseQString() + } + }, + _parseUUID: function () { + this.parseOffset += 16; + return null; + } +} + +exports.AccountInfo = AccountInfo; \ No newline at end of file diff --git a/server-console/src/modules/hf-app.js b/server-console/src/modules/hf-app.js new file mode 100644 index 0000000000..625715b392 --- /dev/null +++ b/server-console/src/modules/hf-app.js @@ -0,0 +1,104 @@ +const fs = require('fs'); +const extend = require('extend'); +const Config = require('./config').Config +const os = require('os'); +const pathFinder = require('./path-finder'); +const path = require('path'); +const argv = require('yargs').argv; +const hfprocess = require('./hf-process'); +const osHomeDir = require('os-homedir'); +const Process = hfprocess.Process; + +const binaryType = argv.binaryType; +const osType = os.type(); + +exports.getBuildInfo = function() { + var buildInfoPath = null; + + if (osType == 'Windows_NT') { + buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); + } else if (osType == 'Darwin') { + var contentPath = ".app/Contents/"; + var contentEndIndex = __dirname.indexOf(contentPath); + + if (contentEndIndex != -1) { + // this is an app bundle + var appPath = __dirname.substring(0, contentEndIndex) + ".app"; + buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); + } + } + + const DEFAULT_BUILD_INFO = { + releaseType: "", + buildIdentifier: "dev", + buildNumber: "0", + stableBuild: "0", + organization: "High Fidelity - dev", + appUserModelId: "com.highfidelity.sandbox-dev" + }; + var buildInfo = DEFAULT_BUILD_INFO; + + if (buildInfoPath) { + try { + buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); + } catch (e) { + buildInfo = DEFAULT_BUILD_INFO; + } + } + + return buildInfo; +} + +const buildInfo = exports.getBuildInfo(); +const interfacePath = pathFinder.discoveredPath("Interface", binaryType, buildInfo.releaseType); + +exports.startInterface = function(url) { + var argArray = []; + + // check if we have a url parameter to include + if (url) { + argArray = ["--url", url]; + } + console.log("Starting with " + url); + // create a new Interface instance - Interface makes sure only one is running at a time + var pInterface = new Process('Interface', interfacePath, argArray); + pInterface.detached = true; + pInterface.start(); +} + +exports.isInterfaceRunning = function(done) { + var pInterface = new Process('interface', 'interface.exe'); + return pInterface.isRunning(done); +} + + +exports.getRootHifiDataDirectory = function(local) { + var organization = buildInfo.organization; + if (osType == 'Windows_NT') { + if (local) { + return path.resolve(osHomeDir(), 'AppData/Local', organization); + } else { + return path.resolve(osHomeDir(), 'AppData/Roaming', organization); + } + } else if (osType == 'Darwin') { + return path.resolve(osHomeDir(), 'Library/Application Support', organization); + } else { + return path.resolve(osHomeDir(), '.local/share/', organization); + } +} + +exports.getDomainServerClientResourcesDirectory = function() { + return path.join(exports.getRootHifiDataDirectory(), '/domain-server'); +} + +exports.getAssignmentClientResourcesDirectory = function() { + return path.join(exports.getRootHifiDataDirectory(), '/assignment-client'); +} + +exports.getApplicationDataDirectory = function(local) { + return path.join(exports.getRootHifiDataDirectory(local), '/Server Console'); +} + +exports.getInterfaceDataDirectory = function(local) { + return path.join(exports.getRootHifiDataDirectory(local), '/Interface'); +} \ No newline at end of file diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js new file mode 100644 index 0000000000..281ca1cb53 --- /dev/null +++ b/server-console/src/modules/hf-notifications.js @@ -0,0 +1,435 @@ +const request = require('request'); +const notifier = require('node-notifier'); +const os = require('os'); +const process = require('process'); +const hfApp = require('./hf-app'); +const path = require('path'); +const AccountInfo = require('./hf-acctinfo').AccountInfo; +const GetBuildInfo = hfApp.getBuildInfo; +const buildInfo = GetBuildInfo(); + +const notificationIcon = path.join(__dirname, '../../resources/console-notification.png'); +const STORIES_NOTIFICATION_POLL_TIME_MS = 120 * 1000; +const PEOPLE_NOTIFICATION_POLL_TIME_MS = 120 * 1000; +const WALLET_NOTIFICATION_POLL_TIME_MS = 600 * 1000; +const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; + +const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' +const STORIES_URL= '/api/v1/user_stories'; +const USERS_URL= '/api/v1/users'; +const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; +const UPDATES_URL= '/api/v1/commerce/available_updates'; +const MAX_NOTIFICATION_ITEMS=30 +const STARTUP_MAX_NOTIFICATION_ITEMS=1 + + +const StartInterface=hfApp.startInterface; +const IsInterfaceRunning=hfApp.isInterfaceRunning; + +const NotificationType = { + GOTO: 'goto', + PEOPLE: 'people', + WALLET: 'wallet', + MARKETPLACE: 'marketplace' +}; + +function HifiNotification(notificationType, notificationData, menuNotificationCallback) { + this.type = notificationType; + this.data = notificationData; +} + +HifiNotification.prototype = { + show: function () { + var text = ""; + var message = ""; + var url = null; + var app = null; + switch (this.type) { + case NotificationType.GOTO: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = "You have " + this.data + " event invitation pending." + } else { + text = "You have " + this.data + " event invitations pending." + } + message = "Click to open GOTO."; + url="hifiapp:GOTO" + } else { + text = this.data.username + " " + this.data.action_string + " in " + this.data.place_name + "."; + message = "Click to go to " + this.data.place_name + "."; + url = "hifi://" + this.data.place_name + this.data.path; + } + break; + + case NotificationType.PEOPLE: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = this.data + " of your connections is online." + } else { + text = this.data + " of your connections are online." + } + message = "Click to open PEOPLE."; + url="hifiapp:PEOPLE" + } else { + text = this.data.username + " is available in " + this.data.location.root.name + "."; + message = "Click to join them."; + url="hifi://" + this.data.location.root.name + this.data.location.path; + } + break; + + case NotificationType.WALLET: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = "You have " + this.data + " unread Wallet transaction."; + } else { + text = "You have " + this.data + " unread Wallet transactions."; + } + message = "Click to open WALLET." + url = "hifiapp:hifi/commerce/wallet/Wallet.qml"; + break; + } + text = this.data.message.replace(/<\/?[^>]+(>|$)/g, ""); + message = "Click to open WALLET."; + url = "hifiapp:WALLET"; + break; + + case NotificationType.MARKETPLACE: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = this.data + " of your purchased items has an update available."; + } else { + text = this.data + " of your purchased items have updates available."; + } + } else { + text = "Update available for " + this.data.base_item_title + "."; + } + message = "Click to open MARKET."; + url = "hifiapp:MARKET"; + break; + } + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: message, + wait: true, + appID: buildInfo.appUserModelId, + url: url + }); + } +} + +function HifiNotifications(config, menuNotificationCallback) { + this.config = config; + this.menuNotificationCallback = menuNotificationCallback; + this.onlineUsers = new Set([]); + this.storiesSince = new Date(this.config.get("storiesNotifySince", "1970-01-01T00:00:00.000Z")); + this.peopleSince = new Date(this.config.get("peopleNotifySince", "1970-01-01T00:00:00.000Z")); + this.walletSince = new Date(this.config.get("walletNotifySince", "1970-01-01T00:00:00.000Z")); + this.marketplaceSince = new Date(this.config.get("marketplaceNotifySince", "1970-01-01T00:00:00.000Z")); + + this.enable(this.enabled()); + + var _menuNotificationCallback = menuNotificationCallback; + notifier.on('click', function (notifierObject, options) { + StartInterface(options.url); + _menuNotificationCallback(options.notificationType, false); + }); +} + +HifiNotifications.prototype = { + enable: function (enabled) { + this.config.set("enableTrayNotifications", enabled); + if (enabled) { + var _this = this; + this.storiesPollTimer = setInterval(function () { + var _since = _this.storiesSince; + _this.storiesSince = new Date(); + _this.pollForStories(_since); + }, + STORIES_NOTIFICATION_POLL_TIME_MS); + + this.peoplePollTimer = setInterval(function () { + var _since = _this.peopleSince; + _this.peopleSince = new Date(); + _this.pollForConnections(_since); + }, + PEOPLE_NOTIFICATION_POLL_TIME_MS); + + this.walletPollTimer = setInterval(function () { + var _since = _this.walletSince; + _this.walletSince = new Date(); + _this.pollForEconomicActivity(_since); + }, + WALLET_NOTIFICATION_POLL_TIME_MS); + + this.marketplacePollTimer = setInterval(function () { + var _since = _this.marketplaceSince; + _this.marketplaceSince = new Date(); + _this.pollForMarketplaceUpdates(_since); + }, + MARKETPLACE_NOTIFICATION_POLL_TIME_MS); + } else { + if (this.storiesPollTimer) { + clearInterval(this.storiesPollTimer); + } + if (this.peoplePollTimer) { + clearInterval(this.peoplePollTimer); + } + if (this.walletPollTimer) { + clearInterval(this.walletPollTimer); + } + if (this.marketplacePollTimer) { + clearInterval(this.marketplacePollTimer); + } + } + }, + enabled: function () { + return this.config.get("enableTrayNotifications", true); + }, + stopPolling: function () { + this.config.set("storiesNotifySince", this.storiesSince.toISOString()); + this.config.set("peopleNotifySince", this.peopleSince.toISOString()); + this.config.set("walletNotifySince", this.walletSince.toISOString()); + this.config.set("marketplaceNotifySince", this.marketplaceSince.toISOString()); + + this.enable(false); + }, + _pollToDisableHighlight: function (notifyType, error, data) { + if (error || !data.body) { + console.log("Error: unable to get " + url); + return false; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return false; + } + if (!content.total_entries) { + this.menuNotificationCallback(notifyType, false); + } + }, + _pollCommon: function (notifyType, url, since, finished) { + + var _this = this; + IsInterfaceRunning(function (running) { + if (running) { + finished(false); + return; + } + var acctInfo = new AccountInfo(); + var token = acctInfo.accessToken(METAVERSE_SERVER_URL); + if (!token) { + return; + } + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + + var maxNotificationItemCount = since.getTime() ? MAX_NOTIFICATION_ITEMS : STARTUP_MAX_NOTIFICATION_ITEMS; + if (error || !data.body) { + console.log("Error: unable to get " + url); + finished(false); + return; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + finished(false); + return; + } + console.log(content); + if (!content.total_entries) { + finished(true, token); + return; + } + _this.menuNotificationCallback(notifyType, true); + if (content.total_entries >= maxNotificationItemCount) { + var notification = new HifiNotification(notifyType, content.total_entries); + notification.show(); + } else { + var notifyData = [] + switch (notifyType) { + case NotificationType.GOTO: + notifyData = content.user_stories; + break; + case NotificationType.PEOPLE: + notifyData = content.data.users; + break; + case NotificationType.WALLET: + notifyData = content.data.history; + break; + case NotificationType.MARKETPLACE: + notifyData = content.data.updates; + break; + } + + notifyData.forEach(function (notifyDataEntry) { + var notification = new HifiNotification(notifyType, notifyDataEntry); + notification.show(); + }); + } + finished(true, token); + }); + }); + }, + pollForStories: function (since) { + var _this = this; + var actions = 'announcement'; + var options = [ + 'since=' + since.getTime() / 1000, + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'per_page='+MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for stories"); + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + console.log(url); + + _this._pollCommon(NotificationType.GOTO, + url, + since, + function (success, token) { + if (success) { + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'per_page=1' + ]; + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + // call a second time to determine if there are no more stories and we should + // put out the light. + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + _this._pollToDisableHighlight(NotificationType.GOTO, error, data); + }); + } + }); + }, + pollForConnections: function (since) { + var _this = this; + var _since = since; + IsInterfaceRunning(function (running) { + if (running) { + return; + } + var options = [ + 'filter=connections', + 'status=online', + 'page=1', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for connections"); + var url = METAVERSE_SERVER_URL + USERS_URL + '?' + options.join('&'); + console.log(url); + var acctInfo = new AccountInfo(); + var token = acctInfo.accessToken(METAVERSE_SERVER_URL); + if (!token) { + return; + } + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + // Users is a special case as we keep track of online users locally. + var maxNotificationItemCount = _since.getTime() ? MAX_NOTIFICATION_ITEMS : STARTUP_MAX_NOTIFICATION_ITEMS; + if (error || !data.body) { + console.log("Error: unable to get " + url); + return false; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return false; + } + console.log(content); + if (!content.total_entries) { + _this.menuNotificationCallback(NotificationType.PEOPLE, false); + _this.onlineUsers = new Set([]); + return; + } + + var currentUsers = new Set([]); + var newUsers = new Set([]); + content.data.users.forEach(function (user) { + currentUsers.add(user.username); + if (!_this.onlineUsers.has(user.username)) { + newUsers.add(user); + _this.onlineUsers.add(user.username); + } + }); + _this.onlineUsers = currentUsers; + if (newUsers.size) { + _this.menuNotificationCallback(NotificationType.PEOPLE, true); + } + + if (newUsers.size >= maxNotificationItemCount) { + var notification = new HifiNotification(NotificationType.PEOPLE, newUsers.size); + notification.show(); + return; + } + newUsers.forEach(function (user) { + var notification = new HifiNotification(NotificationType.PEOPLE, user); + notification.show(); + }); + }); + }); + }, + pollForEconomicActivity: function (since) { + var _this = this; + var options = [ + 'since=' + since.getTime() / 1000, + 'page=1', + 'per_page=' + 1000 // total_entries is incorrect for wallet queries if results + // aren't all on one page, so grab them all on a single page + // for now. + ]; + console.log("Polling for economic activity"); + var url = METAVERSE_SERVER_URL + ECONOMIC_ACTIVITY_URL + '?' + options.join('&'); + console.log(url); + _this._pollCommon(NotificationType.WALLET, url, since, function () {}); + }, + pollForMarketplaceUpdates: function (since) { + var _this = this; + var options = [ + 'since=' + since.getTime() / 1000, + 'page=1', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for marketplace update"); + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + console.log(url); + _this._pollCommon(NotificationType.MARKETPLACE, url, since, function (success, token) { + if (success) { + var options = [ + 'page=1', + 'per_page=1' + ]; + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + _this._pollToDisableHighlight(NotificationType.MARKETPLACE, error, data); + }); + } + }); + } +}; + +exports.HifiNotifications = HifiNotifications; +exports.NotificationType = NotificationType; \ No newline at end of file diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js index 797ee38a0d..cf94ec6b29 100644 --- a/server-console/src/modules/hf-process.js +++ b/server-console/src/modules/hf-process.js @@ -259,6 +259,24 @@ Process.prototype = extend(Process.prototype, { }; return logs; }, + isRunning: function (done) { + var _command = this.command; + if (os.type == 'Windows_NT') { + childProcess.exec('tasklist /FO CSV', function (err, stdout, stderr) { + var running = false; + stdout.split("\n").forEach(function (line) { + var exeData = line.split(","); + var executable = exeData[0].replace(/\"/g, "").toLowerCase(); + if (executable == _command) { + running = true; + } + }); + done(running); + }); + } else if (os.type == 'Darwin') { + console.log("TODO IsRunning Darwin"); + } + }, // Events onChildStartError: function(error) {