diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index b804e4a20f..96e225c7e7 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -368,9 +368,10 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) { return; } - bool sendIdentity = false; + MixerAvatar& avatar = nodeData->getAvatar(); + bool sendIdentity = avatar.needsIdentityUpdate(); if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { - AvatarData& avatar = nodeData->getAvatar(); + MixerAvatar& avatar = nodeData->getAvatar(); const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName(); if (!existingBaseDisplayName.isEmpty()) { SessionDisplayName existingDisplayName { existingBaseDisplayName }; @@ -415,10 +416,11 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) { sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar. // since this packet includes a change to either the skeleton model URL or the display name // it needs a new sequence number - nodeData->getAvatar().pushIdentitySequenceNumber(); + avatar.pushIdentitySequenceNumber(); // tell node whose name changed about its new session display name or avatar. sendIdentityPacket(nodeData, node); + avatar.clearIdentityUpdate(); } } diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp index 3f57bbe3e9..9ad4a0cfd3 100644 --- a/assignment-client/src/avatars/MixerAvatar.cpp +++ b/assignment-client/src/avatars/MixerAvatar.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -78,6 +79,7 @@ void MixerAvatar::fstRequestComplete() { } else { _avatarFSTContents = fstRequest->getData(); _verifyState = kReceivedFST; + _pendingEvent = true; } _avatarRequest->deleteLater(); _avatarRequest = nullptr; @@ -156,12 +158,31 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) { return jsonDocCertifiedItems.toJson(QJsonDocument::Compact); } -void MixerAvatar::processCertifyEvents() { +void MixerAvatar::ownerRequestComplete() { QMutexLocker certifyLocker(&_avatarCertifyLock); - if (_verifyState != kReceivedFST && _verifyState != kOwnerResponse && _verifyState != kChallengeResponse && _verifyState != kRequestingOwner) { + QNetworkReply* networkReply = static_cast(QObject::sender()); + + if (networkReply->error() == QNetworkReply::NoError) { + _dynamicMarketResponse = networkReply->readAll(); + _verifyState = kOwnerResponse; + _pendingEvent = true; + } else { + auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"]; + if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) { + qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":" + << jsonData.toObject()["message"].toString(); + _verifyState = kError; + } + } + networkReply->deleteLater(); +} + +void MixerAvatar::processCertifyEvents() { + if (!_pendingEvent) { return; } + QMutexLocker certifyLocker(&_avatarCertifyLock); switch (_verifyState) { case kReceivedFST: @@ -185,24 +206,10 @@ void MixerAvatar::processCertifyEvents() { request["certificate_id"] = _certificateIdFromFST; _verifyState = kRequestingOwner; QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - //networkReply->setParent(this); - connect(networkReply, &QNetworkReply::readyRead, [this, networkReply]() { - QMutexLocker certifyLocker(&_avatarCertifyLock); - if (networkReply->error() == QNetworkReply::NoError) { - _dynamicMarketResponse = networkReply->readAll(); - _verifyState = kOwnerResponse; - } else { - auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"]; - if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) { - qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":" - << jsonData.toObject()["message"].toString(); - _verifyState = kError; - } - } - networkReply->deleteLater(); - }); + connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete); } else { - _verifyState = kVerificationFailedPending; + _verifyState = kVerificationFailed; + _pendingEvent = false; qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification"; } break; @@ -244,6 +251,7 @@ void MixerAvatar::processCertifyEvents() { "message:" << responseJson["message"].toString(); _verifyState = kError; } + _pendingEvent = false; break; } @@ -266,19 +274,19 @@ void MixerAvatar::processCertifyEvents() { bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash, QByteArray::fromBase64(signedNonce)); - _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailedPending; - if (_verifyState == kVerificationFailedPending) { + _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailed; + _needsIdentityUpdate = true; + if (_verifyState == kVerificationFailed) { qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID(); } else { qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID(); } - + _pendingEvent = false; break; } case kRequestingOwner: - { - certifyLocker.unlock(); + { // Qt networking done on this thread: QCoreApplication::processEvents(); break; } @@ -307,6 +315,7 @@ void MixerAvatar::sendOwnerChallenge() { _challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS); _challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() { _verifyState = kVerificationFailed; + _needsIdentityUpdate = true; }); } @@ -318,5 +327,6 @@ void MixerAvatar::handleChallengeResponse(ReceivedMessage * response) { _challengeTimeout.stop(); _challengeResponse = response->readAll(); _verifyState = kChallengeResponse; + _pendingEvent = true; } } diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 8979d5c9ad..5a81001ea9 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -25,7 +25,15 @@ public: void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } void fetchAvatarFST(); - virtual bool isCertifyFailed() const override { return _verifyState == kVerificationFailed || _verifyState == kVerificationFailedPending; } + virtual bool isCertifyFailed() const override { return _verifyState == kVerificationFailed; } + bool needsIdentityUpdate() const { return _needsIdentityUpdate; } + void clearIdentityUpdate() { _needsIdentityUpdate = false; } + + + //bool isPendingCertifyFailed() const { return _verifyState == kVerificationFailedPending; } + //void advanceCertifyFailed() { + // if (isPendingCertifyFailed()) { _verifyState = kVerificationFailed; } + //} void processCertifyEvents(); void handleChallengeResponse(ReceivedMessage * response); @@ -34,10 +42,11 @@ private: // Avatar certification/verification: enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse, - kChallengeClient, kChallengeResponse, kVerified, kVerificationFailedPending, kVerificationFailed, + kChallengeClient, kChallengeResponse, kVerified, kVerificationFailed, kVerificationSucceeded, kError }; Q_ENUM(VerifyState); VerifyState _verifyState { kNoncertified }; + std::atomic _pendingEvent { false }; QMutex _avatarCertifyLock; ResourceRequest* _avatarRequest { nullptr }; QString _marketplaceIdFromURL; @@ -51,6 +60,7 @@ private: QByteArray _challengeNonceHash; QByteArray _challengeResponse; QTimer _challengeTimeout; + bool _needsIdentityUpdate { false }; bool generateFSTHash(); bool validateFSTHash(const QString& publicKey); @@ -59,6 +69,7 @@ private: private slots: void fstRequestComplete(); + void ownerRequestComplete(); }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/interface/resources/images/AvatarTheftBanner.png b/interface/resources/images/AvatarTheftBanner.png new file mode 100644 index 0000000000..3dc76999e0 Binary files /dev/null and b/interface/resources/images/AvatarTheftBanner.png differ diff --git a/interface/resources/qml/AvatarTheft.qml b/interface/resources/qml/AvatarTheft.qml new file mode 100644 index 0000000000..5c67e64589 --- /dev/null +++ b/interface/resources/qml/AvatarTheft.qml @@ -0,0 +1,70 @@ +import QtQuick 2.7 +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit + +Rectangle { + color: "black" + height: 480 + width: 720 + + readonly property string avatarTheftEntityName: "hifi-avatar-theft-banner"; + + HifiStylesUit.RalewayRegular { + id: displayMessage; + text: "The avatar you're using is registered to another user."; + size: 20; + color: "white"; + anchors.top: parent.top; + anchors.topMargin: 0.1 * parent.height; + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + HifiStylesUit.ShortcutText { + id: gotoShortcut; + anchors.top: parent.top; + anchors.topMargin: 0.4 * parent.height; + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + font.family: "Raleway" + font.pixelSize: 20 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + text: "Click here to change your avatar and dismiss this banner." + onLinkActivated: { + AddressManager.handleLookupString("hifi://BodyMart"); + } + } + + HifiStylesUit.RalewayRegular { + id: contactText; + text: "If you own this avatar, please contact" + size: 20; + anchors.bottom: parent.bottom + anchors.bottomMargin: 0.15 * parent.height + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + } + + HifiStylesUit.ShortcutText { + id: gotoShortcut; + anchors.top: contactText.bottom; + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + font.family: "Raleway" + font.pixelSize: 20 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + text: "Click here to change your avatar and dismiss this banner." + onLinkActivated: { + HiFiAbout.openUrl("mailto:support@highfidelity.com"); + } + } +} diff --git a/interface/resources/qml/AvatarTheftBanner.qml b/interface/resources/qml/AvatarTheftBanner.qml new file mode 100644 index 0000000000..e2c437487e --- /dev/null +++ b/interface/resources/qml/AvatarTheftBanner.qml @@ -0,0 +1,67 @@ +import QtQuick 2.7 +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit + +Rectangle { + color: "black" + + HifiStylesUit.HifiConstants { id: hifi; } + + HifiStylesUit.RalewayRegular { + id: displayMessage; + text: "The avatar you're using is registered to another user."; + size: 20; + color: "white"; + anchors.top: parent.top; + anchors.topMargin: 0.1 * parent.height; + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + HifiStylesUit.ShortcutText { + id: gotoShortcut; + anchors.top: parent.top; + anchors.topMargin: 0.4 * parent.height; + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + font.family: "Raleway" + font.pixelSize: 20 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + text: "Click here to change your avatar and dismiss this banner." + onLinkActivated: { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.loadQMLSource("hifi/AvatarApp.qml"); + } + } + + HifiStylesUit.RalewayRegular { + id: contactText; + text: "If you own this avatar, please contact" + size: 20; + color: "white" + anchors.bottom: parent.bottom + anchors.bottomMargin: 0.15 * parent.height + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + } + + HifiStylesUit.ShortcutText { + id: email; + anchors.top: contactText.bottom; + anchors.left: parent.left; + anchors.leftMargin: (parent.width - width) / 2 + font.family: "Raleway" + font.pixelSize: 20 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + text: "support@highfidelity.com." + } +} diff --git a/interface/resources/qml/AvatarTheftSettings.qml b/interface/resources/qml/AvatarTheftSettings.qml new file mode 100644 index 0000000000..849e611af2 --- /dev/null +++ b/interface/resources/qml/AvatarTheftSettings.qml @@ -0,0 +1,139 @@ +import QtQuick 2.10 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit + +Rectangle { + id: root; + + HifiConstants { id: hifi; } + + color: hifi.colors.baseGray; + + signal sendToScript(var message); + function emitSendToScript(message) { + sendToScript(message); + } + + function fromScript(message) { + } + + RalewayRegular { + id: title; + color: hifi.colors.white; + text: qsTr("Avatar Theft Entity position") + size: 20 + font.bold: true + anchors { + top: parent.top + left: parent.left + leftMargin: (parent.width - width) / 2 + } + } + + HifiControlsUit.Slider { + id: xSlider + anchors { + top: title.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "X OFFSET: " + value.toFixed(2); + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: 0.0 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "x": value + }); + } + } + + HifiControlsUit.Slider { + id: ySlider + anchors { + top: xSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "Y OFFSET: " + value.toFixed(2); + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: 0.0 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "y": value + }); + } + } + + HifiControlsUit.Slider { + id: zSlider + anchors { + top: ySlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "Z OFFSET: " + value.toFixed(2); + maximumValue: 0.0 + minimumValue: -2.0 + stepSize: 0.05 + value: -1.0 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "z": value + }); + } + } + + HifiControlsUit.Button { + id: setVisibleButton; + text: setVisible ? "SET INVISIBLE" : "SET VISIBLE"; + width: 300; + property bool setVisible: true; + anchors { + top: zSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + setVisible = !setVisible; + emitSendToScript({ + "method": "setVisible", + "visible": setVisible + }); + } + } + + HifiControlsUit.Button { + id: printButton; + text: "PRINT POSITIONS"; + width: 300; + anchors { + top: setVisibleButton.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + emitSendToScript({ + "method": "print", + }); + } + } +} + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7ba45da1fd..55a6fc6672 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3368,6 +3368,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); if (setAdditionalContextProperties) { + qDebug() << "setting additional context properties!"; auto tabletScriptingInterface = DependencyManager::get(); auto flags = tabletScriptingInterface->getFlags(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 8274259922..390500dd2a 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -46,6 +46,7 @@ #include "MyAvatar.h" #include "DebugDraw.h" #include "SceneScriptingInterface.h" +#include "ui/AvatarCertifyBanner.h" // 50 times per second - target is 45hz, but this helps account for any small deviations // in the update loop - this also results in ~30hz when in desktop mode which is essentially @@ -176,6 +177,13 @@ void AvatarManager::updateMyAvatar(float deltaTime) { _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); } + + static AvatarCertifyBanner theftBanner; + if (_myAvatar->isCertifyFailed()) { + theftBanner.show(_myAvatar->getSessionUUID(), _myAvatar->getJointIndex("_CAMERA_MATRIX")); + } else { + theftBanner.clear(); + } } diff --git a/scripts/developer/tests/avatarTheftPrototype.js b/scripts/developer/tests/avatarTheftPrototype.js new file mode 100644 index 0000000000..0ee5795a17 --- /dev/null +++ b/scripts/developer/tests/avatarTheftPrototype.js @@ -0,0 +1,144 @@ +"use strict"; + +(function() { + + var appUi = Script.require("appUi"); + var ui; + + var AVATAR_THEFT_BANNER_IMAGE = Script.resourcesPath() + "images/AvatarTheftBanner.png"; + var AVATAR_THEFT_SETTINGS_QML = Script.resourcesPath() + "qml/AvatarTheftSettings.qml"; + var button; + var theftBanner = null; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var onAvatarBanner = false; + var THEFT_BANNER_DIMENSIONS = {x: 1.0, y: 1.0, z: 0.3}; + + function createEntities() { + //if (HMD.active) { + var render = tablet.tabletShown ? "world" : "front"; + var pos = tablet.tabletShown ? { x: 0.0, y: 0.0, z: -1.8 } : { x: 0.0, y: 0.0, z: -0.7 }; + var dimensionMultiplier = tablet.tabletShown ? 2.6 : 1; + var props = { + type: "Image", + imageURL: AVATAR_THEFT_BANNER_IMAGE, + name: "hifi-avatar-theft-banner", + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: pos, + dimensions: {x: THEFT_BANNER_DIMENSIONS.x * dimensionMultiplier, y: THEFT_BANNER_DIMENSIONS.y * dimensionMultiplier}, + renderLayer: render, + userData: { + grabbable: false + }, + visible: true + }; +/* var props = {*/ + //type: "Web", + //name: "hifi-avatar-theft-banner", + //sourceUrl: AVATAR_THEFT_BANNER_QML, + //parentID: MyAvatar.SELF_ID, + //parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + //localPosition: { x: 0.0, y: 0.0, z: -1.0 }, + //dimensions: THEFT_BANNER_DIMENSIONS, + //drawInFront: true, + //userData: { + //grabbable: false + //}, + //visible: true + /*};*/ + if (theftBanner) { + Entities.deleteEntity(theftBanner); + } + theftBanner = Entities.addEntity(props, "local"); + Window.copyToClipboard(theftBanner); + console.log("created entity"); + //} else { + //} + } + + function fromQml(message) { + if (message.method === "reposition") { + var theftBannerLocalPosition = Entities.getEntityProperties(theftBanner).localPosition; + var newTheftBannerLocalPosition; + if (message.x !== undefined) { + newTheftBannerLocalPosition = { x: -((THEFT_BANNER_DIMENSIONS.x) / 2) + message.x, y: theftBannerLocalPosition.y, z: theftBannerLocalPosition.z }; + } else if (message.y !== undefined) { + newtheftBannerLocalPosition = { x: theftBannerLocalPosition.x, y: message.y, z: theftBannerLocalPosition.z }; + } else if (message.z !== undefined) { + newtheftBannerLocalPosition = { x: theftBannerLocalPosition.x, y: theftBannerLocalPosition.y, z: message.z }; + } + var theftBannerProps = { + localPosition: newtheftBannerLocalPosition + }; + + Entities.editEntity(theftBanner, theftBannerProps); + } else if (message.method === "setVisible") { + if (message.visible !== undefined) { + var props = { + visible: message.visible + }; + Entities.editEntity(theftBannerEntity, props); + } + } else if (message.method === "print") { + // prints the local position into the hifi log. + var theftBannerLocalPosition = Entities.getEntityProperties(theftBannerEntity).localPosition; + console.log("theft banner local position is at " + JSON.stringify(theftBannerLocalPosition)); + } + }; + + var cleanup = function () { + if (theftBanner) { + Entities.deleteEntity(theftBanner); + } + }; + + function setup() { + ui = new appUi({ + buttonName: "THEFT", + home: AVATAR_THEFT_SETTINGS_QML, + onMessage: fromQml, + onOpened: createEntities, + onClosed: cleanup, + normalButton: "icons/tablet-icons/edit-i.svg", + activeButton: "icons/tablet-icons/edit-a.svg", + }); + }; + + setup(); + + Entities.mousePressOnEntity.connect(function (entityID, event) { + if (entityID === theftBanner && theftBanner){ + tablet.loadQMLSource(Script.resourcesPath() + "qml/hifi/AvatarApp.qml"); + } + }) + + tablet.isTabletShownChanged.connect(function () { + if (theftBanner) { + if (tablet.tabletShown) { + Entities.editEntity(theftBanner, { + dimensions: THEFT_BANNER_DIMENSIONS, + localPosition: { x: 0.0, y: 0.0, z: -0.7 }, + renderLayer: "world" + }); + } else { + Entities.editEntity(theftBanner, { + localPosition: { x: 0.0, y: 0.0, z: -1.8 }, + dimensions: {x: THEFT_BANNER_DIMENSIONS.x * 2.6, y: THEFT_BANNER_DIMENSIONS.y * 2.6}, + renderLayer: "front" + }); + } + console.log(Entities.getEntityProperties(theftBanner).renderLayer); + } + }) + + Script.scriptEnding.connect(cleanup); + + location.hostChanged.connect(function (){ + if (theftBanner) { + Entities.editEntity(theftBanner, { + visible: false + }); + } + }) + +}());