diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index c6bcc5af34..f4d7d324a6 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -782,21 +782,24 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer message, SharedNodePointer sendingNode) { // From the packet, pull the UUID we're identifying QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - if (!nodeUUID.isNull()) { - // Before we do any processing on this packet, make sure it comes from a node that is allowed to kick (is an admin) - // OR from a node whose UUID matches the one in the packet - if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) { - // First, make sure we actually have a node with this UUID - auto limitedNodeList = DependencyManager::get(); - auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID); - // If we do have a matching node... - if (matchingNode) { + if (!nodeUUID.isNull()) { + // First, make sure we actually have a node with this UUID + auto limitedNodeList = DependencyManager::get(); + auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID); + + // If we do have a matching node... + if (matchingNode) { + // Setup the packet + auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply); + + bool isAdmin = matchingNode->getCanKick(); + + // Check if the sending node has permission to kick (is an admin) + if (sendingNode->getCanKick()) { // It's time to figure out the username QString verifiedUsername = matchingNode->getPermissions().getVerifiedUserName(); - // Setup the packet - auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply); usernameFromIDReplyPacket->write(nodeUUID.toRfc4122()); usernameFromIDReplyPacket->writeString(verifiedUsername); @@ -804,19 +807,20 @@ void DomainServerSettingsManager::processUsernameFromIDRequestPacket(QSharedPoin DomainServerNodeData* nodeData = reinterpret_cast(matchingNode->getLinkedData()); QUuid machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid(); usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122()); - - qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID; - - // Ship it! - limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode); + qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID << ". Node admin status: " << isAdmin; } else { - qWarning() << "Node username request received for unknown node. Refusing to process."; + usernameFromIDReplyPacket->write(nodeUUID.toRfc4122()); + usernameFromIDReplyPacket->writeString(""); + usernameFromIDReplyPacket->writeString(""); } + // Write whether or not the user is an admin + usernameFromIDReplyPacket->writePrimitive(isAdmin); + + // Ship it! + limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode); } else { - qWarning() << "Refusing to process a username request packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()) - << ". Either node doesn't have kick permissions or is requesting a username not from their UUID."; + qWarning() << "Node username request received for unknown node. Refusing to process."; } - } else { qWarning() << "Node username request received for invalid node ID. Refusing to process."; } diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 2725ea1617..1ad3fae7c1 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -33,6 +33,7 @@ Item { property real audioLevel: 0.0 property bool isMyCard: false property bool selected: false + property bool isAdmin: false /* User image commented out for now - will probably be re-introduced later. Column { @@ -139,32 +140,84 @@ Item { } } // Spacer for DisplayName for my card - Rectangle { + Item { id: myDisplayNameSpacer - width: myDisplayName.width + width: 1 + height: 4 // Anchors anchors.top: myDisplayName.bottom - height: 5 - visible: isMyCard - opacity: 0 } - // DisplayName Text for others' cards - FiraSansSemiBold { - id: displayNameText - // Properties - text: thisNameCard.displayName - elide: Text.ElideRight + // DisplayName container for others' cards + Item { + id: displayNameContainer visible: !isMyCard // Size width: parent.width + height: displayNameTextPixelSize + 4 // Anchors anchors.top: parent.top - // Text Size - size: displayNameTextPixelSize - // Text Positioning - verticalAlignment: Text.AlignVCenter - // Style - color: hifi.colors.darkGray + anchors.left: parent.left + // DisplayName Text for others' cards + FiraSansSemiBold { + id: displayNameText + // Properties + text: thisNameCard.displayName + elide: Text.ElideRight + // Anchors + anchors.top: parent.top + anchors.left: parent.left + // Text Size + size: displayNameTextPixelSize + // Text Positioning + verticalAlignment: Text.AlignVCenter + // Style + color: hifi.colors.darkGray + } + // "ADMIN" label for other users' cards + RalewaySemiBold { + id: adminLabelText + text: "ADMIN" + // Text size + size: displayNameText.size - 4 + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: displayNameText.right + anchors.leftMargin: 8 + // Style + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + } + // This Rectangle refers to the [?] popup button next to "ADMIN" + Item { + // Size + width: 20 + height: displayNameText.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: adminLabelText.right + RalewayRegular { + id: adminLabelQuestionMark + text: "[?]" + size: adminLabelText.size + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: letterbox('Silencing a user mutes their microphone. Silenced users can unmute themselves by clicking the "UNMUTE" button on their HUD.\n\n' + + "Banning a user will remove them from this domain and prevent them from returning. You can un-ban users from your domain's settings page.)") + onEntered: adminLabelQuestionMark.color = "#94132e" + onExited: adminLabelQuestionMark.color = hifi.colors.redHighlight + } + } } // UserName Text @@ -177,7 +230,7 @@ Item { // Size width: parent.width // Anchors - anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameText.bottom + anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom // Text Size size: thisNameCard.usernameTextHeight // Text Positioning @@ -188,7 +241,7 @@ Item { // Spacer Item { - id: spacer + id: userNameSpacer height: 4 width: parent.width // Anchors @@ -202,7 +255,7 @@ Item { width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width height: 8 // Anchors - anchors.top: spacer.bottom + anchors.top: userNameSpacer.bottom // Style radius: 4 color: "#c5c5c5" diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 549598dd2e..ef1203ffa4 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -221,6 +221,7 @@ Rectangle { visible: !isCheckBox && !isButton uuid: model && model.sessionId selected: styleData.selected + isAdmin: model && model.isAdmin // Size width: nameCardWidth height: parent.height @@ -465,6 +466,7 @@ Rectangle { var userId = message.params[0]; // The text that goes in the userName field is the second parameter in the message. var userName = message.params[1]; + var isAdmin = message.params[2]; // If the userId is empty, we're updating "myData". if (!userId) { myData.userName = userName; @@ -476,6 +478,9 @@ Rectangle { // Set the userName appropriately userModel.setProperty(userIndex, "userName", userName); userModelData[userIndex].userName = userName; // Defensive programming + // Set the admin status appropriately + userModel.setProperty(userIndex, "isAdmin", isAdmin); + userModelData[userIndex].isAdmin = isAdmin; // Defensive programming } else { console.log("updateUsername() called with unknown UUID: ", userId); } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 0fcd207b94..7c5e7fc5b7 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -1068,10 +1068,12 @@ void NodeList::processUsernameFromIDReply(QSharedPointer messag QString username = message->readString(); // read the machine fingerprint from the packet QString machineFingerprintString = (QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID))).toString(); + bool isAdmin; + message->readPrimitive(&isAdmin); qCDebug(networking) << "Got username" << username << "and machine fingerprint" << machineFingerprintString << "for node" << nodeUUIDString; - emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString); + emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString, isAdmin); } void NodeList::setRequestsDomainListData(bool isRequesting) { diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index c4564c0889..0e0a2fd6c8 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -120,7 +120,7 @@ signals: void receivedDomainServerList(); void ignoredNode(const QUuid& nodeID, bool enabled); void ignoreRadiusEnabledChanged(bool isIgnored); - void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint, bool isAdmin); private slots: void stopKeepalivePingTimer(); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 758868ac63..5ae1d5a75b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -135,9 +135,10 @@ signals: /**jsdoc * Notifies scripts of the username and machine fingerprint associated with a UUID. + * Username and machineFingerprint will be empty strings if the requesting user isn't an admin. * @function Users.usernameFromIDReply */ - void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint, bool isAdmin); /**jsdoc * Notifies scripts that a user has disconnected from the domain diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 2430ce6e87..b95e9bdb87 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -269,12 +269,9 @@ function populateUserList() { sessionId: id || '', audioLevel: 0.0 }; - // If the current user is an admin OR - // they're requesting their own username ("id" is blank)... - if (Users.canKick || !id) { - // Request the username from the given UUID - Users.requestUsernameFromID(id); - } + // Request the username, fingerprint, and admin status from the given UUID + // Username and fingerprint returns "" if the requesting user isn't an admin + Users.requestUsernameFromID(id); // Request personal mute status and ignore status // from NodeList (as long as we're not requesting it for our own ID) if (id) { @@ -289,7 +286,7 @@ function populateUserList() { } // The function that handles the reply from the server -function usernameFromIDReply(id, username, machineFingerprint) { +function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { var data; // If the ID we've received is our ID... if (MyAvatar.sessionUUID === id) { @@ -300,6 +297,7 @@ function usernameFromIDReply(id, username, machineFingerprint) { // or fingerprint (if we don't have a username) string. data = [id, username || machineFingerprint]; } + data.push(isAdmin); print('Username Data:', JSON.stringify(data)); // Ship the data off to QML pal.sendToQml({ method: 'updateUsername', params: data });