diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index cac0d28e1e..ba00392cd7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.2, + "version": 1.3, "settings": [ { "name": "metaverse", @@ -71,6 +71,76 @@ } ] }, + { + "name": "descriptors", + "label": "Description", + "help": "This data will be queryable from your server. It may be collected by High Fidelity and used to share your domain with others.", + "settings": [ + { + "name": "description", + "label": "Description", + "help": "A description of your domain (256 character limit)." + }, + { + "name": "maturity", + "label": "Maturity", + "help": "A maturity rating, available as a guideline for content on your domain.", + "default": "unrated", + "type": "select", + "options": [ + { + "value": "unrated", + "label": "Unrated" + }, + { + "value": "everyone", + "label": "Everyone" + }, + { + "value": "teen", + "label": "Teen (13+)" + }, + { + "value": "mature", + "label": "Mature (17+)" + }, + { + "value": "adult", + "label": "Adult (18+)" + } + ] + + }, + { + "name": "hosts", + "label": "Hosts", + "type": "table", + "help": "Usernames of hosts who can reliably show your domain to new visitors.", + "numbered": false, + "columns": [ + { + "name": "host", + "label": "Username", + "can_set": true + } + ] + }, + { + "name": "tags", + "label": "Tags", + "type": "table", + "help": "Common categories under which your domain falls.", + "numbered": false, + "columns": [ + { + "name": "tag", + "label": "Tag", + "can_set": true + } + ] + } + ] + }, { "name": "security", "label": "Security", diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp new file mode 100644 index 0000000000..26d2bb87ce --- /dev/null +++ b/domain-server/src/DomainMetadata.cpp @@ -0,0 +1,132 @@ +// +// DomainMetadata.cpp +// domain-server/src +// +// Created by Zach Pomerantz on 5/25/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "DomainMetadata.h" + +#include +#include +#include + +#include "DomainServerNodeData.h" + +const QString DomainMetadata::USERS = "users"; +const QString DomainMetadata::USERS_NUM_TOTAL = "num_users"; +const QString DomainMetadata::USERS_NUM_ANON = "num_anon_users"; +const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames"; +// users metadata will appear as (JSON): +// { "num_users": Number, +// "num_anon_users": Number, +// "user_hostnames": { : Number } +// } + +const QString DomainMetadata::DESCRIPTORS = "descriptors"; +const QString DomainMetadata::DESCRIPTORS_DESCRIPTION = "description"; +const QString DomainMetadata::DESCRIPTORS_CAPACITY = "capacity"; // parsed from security +const QString DomainMetadata::DESCRIPTORS_RESTRICTION = "restriction"; // parsed from ACL +const QString DomainMetadata::DESCRIPTORS_MATURITY = "maturity"; +const QString DomainMetadata::DESCRIPTORS_HOSTS = "hosts"; +const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; +// descriptors metadata will appear as (JSON): +// { "capacity": Number, +// TODO: "hours": String, // UTF-8 representation of the week, split into 15" segments +// "restriction": String, // enum of either open, hifi, or acl +// "maturity": String, // enum corresponding to ESRB ratings +// "hosts": [ String ], // capped list of usernames +// "description": String, // capped description +// TODO: "img": { +// "src": String, +// "type": String, +// "size": Number, +// "updated_at": Number, +// }, +// "tags": [ String ], // capped list of tags +// } + +// metadata will appear as (JSON): +// { users: , descriptors: } +// +// it is meant to be sent to and consumed by an external API + +DomainMetadata::DomainMetadata() { + _metadata[USERS] = {}; + _metadata[DESCRIPTORS] = {}; +} + +void DomainMetadata::setDescriptors(QVariantMap& settings) { + const QString CAPACITY = "security.maximum_user_capacity"; + const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); + unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; + + // TODO: Keep parity with ACL development. + const QString RESTRICTION = "security.restricted_access"; + const QString RESTRICTION_OPEN = "open"; + // const QString RESTRICTION_HIFI = "hifi"; + const QString RESTRICTION_ACL = "acl"; + const QVariant* isRestrictedVariant = valueForKeyPath(settings, RESTRICTION); + bool isRestricted = isRestrictedVariant ? isRestrictedVariant->toBool() : false; + QString restriction = isRestricted ? RESTRICTION_ACL : RESTRICTION_OPEN; + + QVariantMap descriptors = settings[DESCRIPTORS].toMap(); + descriptors[DESCRIPTORS_CAPACITY] = capacity; + descriptors[DESCRIPTORS_RESTRICTION] = restriction; + _metadata[DESCRIPTORS] = descriptors; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata descriptors set:" << descriptors; +#endif +} + +void DomainMetadata::updateUsers() { + static const QString DEFAULT_HOSTNAME = "*"; + + auto nodeList = DependencyManager::get(); + int numConnected = 0; + int numConnectedAnonymously = 0; + QVariantMap userHostnames; + + // figure out the breakdown of currently connected interface clients + nodeList->eachNode([&numConnected, &numConnectedAnonymously, &userHostnames](const SharedNodePointer& node) { + auto linkedData = node->getLinkedData(); + if (linkedData) { + auto nodeData = static_cast(linkedData); + + if (!nodeData->wasAssigned()) { + ++numConnected; + + if (nodeData->getUsername().isEmpty()) { + ++numConnectedAnonymously; + } + + // increment the count for this hostname (or the default if we don't have one) + auto placeName = nodeData->getPlaceName(); + auto hostname = placeName.isEmpty() ? DEFAULT_HOSTNAME : placeName; + userHostnames[hostname] = userHostnames[hostname].toInt() + 1; + } + } + }); + + QVariantMap users = { + { USERS_NUM_TOTAL, numConnected }, + { USERS_NUM_ANON, numConnectedAnonymously }, + { USERS_HOSTNAMES, userHostnames }}; + _metadata[USERS] = users; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata users updated:" << users; +#endif +} + +void DomainMetadata::usersChanged() { + ++_tic; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata users change detected"; +#endif +} diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h new file mode 100644 index 0000000000..7d58d43182 --- /dev/null +++ b/domain-server/src/DomainMetadata.h @@ -0,0 +1,65 @@ +// +// DomainMetadata.h +// domain-server/src +// +// Created by Zach Pomerantz on 5/25/2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#ifndef hifi_DomainMetadata_h +#define hifi_DomainMetadata_h + +#include + +#include +#include + +class DomainMetadata : public QObject { +Q_OBJECT + + static const QString USERS; + static const QString USERS_NUM_TOTAL; + static const QString USERS_NUM_ANON; + static const QString USERS_HOSTNAMES; + + static const QString DESCRIPTORS; + static const QString DESCRIPTORS_DESCRIPTION; + static const QString DESCRIPTORS_CAPACITY; + static const QString DESCRIPTORS_HOURS; + static const QString DESCRIPTORS_RESTRICTION; + static const QString DESCRIPTORS_MATURITY; + static const QString DESCRIPTORS_HOSTS; + static const QString DESCRIPTORS_TAGS; + static const QString DESCRIPTORS_IMG; + static const QString DESCRIPTORS_IMG_SRC; + static const QString DESCRIPTORS_IMG_TYPE; + static const QString DESCRIPTORS_IMG_SIZE; + static const QString DESCRIPTORS_IMG_UPDATED_AT; + +public: + DomainMetadata(); + + // Returns the last set metadata + // If connected users have changed, metadata may need to be updated + // this should be checked by storing tic = getTic() between calls + // and testing it for equality before the next get (tic == getTic()) + QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); } + QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS].toMap()); } + QJsonObject getDescriptors() { return QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); } + + uint32_t getTic() { return _tic; } + + void setDescriptors(QVariantMap& settings); + void updateUsers(); + +public slots: + void usersChanged(); + +protected: + QVariantMap _metadata; + uint32_t _tic{ 0 }; +}; + +#endif // hifi_DomainMetadata_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 052a7c0fec..0f5498a575 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -94,6 +94,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); + // update the metadata when a user (dis)connects + connect(this, &DomainServer::userConnected, &_metadata, &DomainMetadata::usersChanged); + connect(this, &DomainServer::userDisconnected, &_metadata, &DomainMetadata::usersChanged); + // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); @@ -112,6 +116,9 @@ DomainServer::DomainServer(int argc, char* argv[]) : optionallyGetTemporaryName(args); } + + // update the metadata with current descriptors + _metadata.setDescriptors(_settingsManager.getSettingsMap()); } DomainServer::~DomainServer() { @@ -767,12 +774,16 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { } void DomainServer::handleConnectedNode(SharedNodePointer newNode) { - - DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + DomainServerNodeData* nodeData = static_cast(newNode->getLinkedData()); + // reply back to the user with a PacketType::DomainList sendDomainListToNode(newNode, nodeData->getSendingSockAddr()); - + + // if this node is a user (unassigned Agent), signal + if (newNode->getType() == NodeType::Agent && !nodeData->wasAssigned()) { + emit userConnected(); + } + // send out this node to our other connected nodes broadcastNewNode(newNode); } @@ -1067,62 +1078,39 @@ void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) sendHeartbeatToMetaverse(newPublicSockAddr.getAddress().toString()); } - void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { - const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - auto nodeList = DependencyManager::get(); const QUuid& domainID = nodeList->getSessionUUID(); - // setup the domain object to send to the data server - const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; - const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; - + // Setup the domain object to send to the data server QJsonObject domainObject; + if (!networkAddress.isEmpty()) { + static const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; } + static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings - const QString RESTRICTED_ACCESS_FLAG = "restricted"; - + // Add a flag to indicate if this domain uses restricted access - + // for now that will exclude it from listings + static const QString RESTRICTED_ACCESS_FLAG = "restricted"; domainObject[RESTRICTED_ACCESS_FLAG] = _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - // figure out the breakdown of currently connected interface clients - int numConnectedUnassigned = 0; - QJsonObject userHostnames; - - static const QString DEFAULT_HOSTNAME = "*"; - - nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { - if (node->getLinkedData()) { - auto nodeData = static_cast(node->getLinkedData()); - - if (!nodeData->wasAssigned()) { - ++numConnectedUnassigned; - - // increment the count for this hostname (or the default if we don't have one) - auto hostname = nodeData->getPlaceName().isEmpty() ? DEFAULT_HOSTNAME : nodeData->getPlaceName(); - userHostnames[hostname] = userHostnames[hostname].toInt() + 1; - } - } - }); - + // Add the metadata to the heartbeat static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; - static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; + auto tic = _metadata.getTic(); + if (_metadataTic != tic) { + _metadataTic = tic; + _metadata.updateUsers(); + } + domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.getUsers(); - QJsonObject heartbeatObject; - heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; - heartbeatObject[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; - - domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; - - QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); + QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); + static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, @@ -1918,11 +1906,10 @@ void DomainServer::nodeAdded(SharedNodePointer node) { } void DomainServer::nodeKilled(SharedNodePointer node) { - // if this peer connected via ICE then remove them from our ICE peers hash _gatekeeper.removeICEPeer(node->getUUID()); - DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); + DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); if (nodeData) { // if this node's UUID matches a static assignment we need to throw it back in the assignment queue @@ -1934,15 +1921,22 @@ void DomainServer::nodeKilled(SharedNodePointer node) { } } - // If this node was an Agent ask DomainServerNodeData to potentially remove the interpolation we stored - nodeData->removeOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, - uuidStringWithoutCurlyBraces(node->getUUID())); - // cleanup the connection secrets that we set up for this node (on the other nodes) foreach (const QUuid& otherNodeSessionUUID, nodeData->getSessionSecretHash().keys()) { SharedNodePointer otherNode = DependencyManager::get()->nodeWithUUID(otherNodeSessionUUID); if (otherNode) { - reinterpret_cast(otherNode->getLinkedData())->getSessionSecretHash().remove(node->getUUID()); + static_cast(otherNode->getLinkedData())->getSessionSecretHash().remove(node->getUUID()); + } + } + + if (node->getType() == NodeType::Agent) { + // if this node was an Agent ask DomainServerNodeData to remove the interpolation we potentially stored + nodeData->removeOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, + uuidStringWithoutCurlyBraces(node->getUUID())); + + // if this node is a user (unassigned Agent), signal + if (!nodeData->wasAssigned()) { + emit userDisconnected(); } } } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c39e405380..8b8409ff0a 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -26,6 +26,7 @@ #include #include "DomainGatekeeper.h" +#include "DomainMetadata.h" #include "DomainServerSettingsManager.h" #include "DomainServerWebSessionData.h" #include "WalletTransaction.h" @@ -91,6 +92,8 @@ private slots: signals: void iceServerChanged(); + void userConnected(); + void userDisconnected(); private: void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); @@ -167,6 +170,9 @@ private: DomainServerSettingsManager _settingsManager; + DomainMetadata _metadata; + uint32_t _metadataTic{ 0 }; + HifiSockAddr _iceServerSocket; std::unique_ptr _iceServerHeartbeatPacket;