diff options
author | Felix Weilbach <felix.weilbach@nextcloud.com> | 2021-09-09 12:18:22 +0300 |
---|---|---|
committer | Felix Weilbach <felix.weilbach@nextcloud.com> | 2021-09-09 12:18:22 +0300 |
commit | 8a8d488454405356b5d11f63bebff2d69be43b02 (patch) | |
tree | afa6c5a496561e6778227ece22a6c871622af926 /src/libsync | |
parent | f34d66302942ede371bf7bc5d5e213b6c41ea5d8 (diff) |
Add dialog to set user status
Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
Diffstat (limited to 'src/libsync')
-rw-r--r-- | src/libsync/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/libsync/abstractnetworkjob.cpp | 9 | ||||
-rw-r--r-- | src/libsync/abstractnetworkjob.h | 3 | ||||
-rw-r--r-- | src/libsync/account.cpp | 37 | ||||
-rw-r--r-- | src/libsync/account.h | 11 | ||||
-rw-r--r-- | src/libsync/capabilities.cpp | 20 | ||||
-rw-r--r-- | src/libsync/capabilities.h | 2 | ||||
-rw-r--r-- | src/libsync/clientsideencryption.cpp | 13 | ||||
-rw-r--r-- | src/libsync/clientsideencryption.h | 2 | ||||
-rw-r--r-- | src/libsync/clientsideencryptionjobs.cpp | 17 | ||||
-rw-r--r-- | src/libsync/datetimeprovider.cpp | 17 | ||||
-rw-r--r-- | src/libsync/datetimeprovider.h | 18 | ||||
-rw-r--r-- | src/libsync/networkjobs.cpp | 38 | ||||
-rw-r--r-- | src/libsync/networkjobs.h | 25 | ||||
-rw-r--r-- | src/libsync/ocsuserstatusconnector.cpp | 455 | ||||
-rw-r--r-- | src/libsync/ocsuserstatusconnector.h | 70 | ||||
-rw-r--r-- | src/libsync/userstatusconnector.cpp | 121 | ||||
-rw-r--r-- | src/libsync/userstatusconnector.h | 138 |
18 files changed, 971 insertions, 28 deletions
diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 3c38c6374..750d45ec8 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -55,6 +55,9 @@ set(libsync_SRCS theme.cpp clientsideencryption.cpp clientsideencryptionjobs.cpp + datetimeprovider.cpp + ocsuserstatusconnector.cpp + userstatusconnector.cpp creds/dummycredentials.cpp creds/abstractcredentials.cpp creds/credentialscommon.cpp diff --git a/src/libsync/abstractnetworkjob.cpp b/src/libsync/abstractnetworkjob.cpp index be9c01c93..6bb00a6b2 100644 --- a/src/libsync/abstractnetworkjob.cpp +++ b/src/libsync/abstractnetworkjob.cpp @@ -139,6 +139,15 @@ QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUr return reply; } +QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUrl &url, + QNetworkRequest req, const QByteArray &requestBody) +{ + auto reply = _account->sendRawRequest(verb, url, req, requestBody); + _requestBody = nullptr; + adoptRequest(reply); + return reply; +} + void AbstractNetworkJob::adoptRequest(QNetworkReply *reply) { addTimer(reply); diff --git a/src/libsync/abstractnetworkjob.h b/src/libsync/abstractnetworkjob.h index d1f3f6bc5..63391e672 100644 --- a/src/libsync/abstractnetworkjob.h +++ b/src/libsync/abstractnetworkjob.h @@ -128,6 +128,9 @@ protected: QNetworkRequest req = QNetworkRequest(), QIODevice *requestBody = nullptr); + QNetworkReply *sendRequest(const QByteArray &verb, const QUrl &url, + QNetworkRequest req, const QByteArray &requestBody); + // sendRequest does not take a relative path instead of an url, // but the old API allowed that. We have this undefined overload // to help catch usage errors diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 8beb34412..d0cfe36e4 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -13,6 +13,8 @@ */ #include "account.h" +#include "accountfwd.h" +#include "clientsideencryptionjobs.h" #include "cookiejar.h" #include "networkjobs.h" #include "configfile.h" @@ -27,6 +29,7 @@ #include "common/asserts.h" #include "clientsideencryption.h" +#include "ocsuserstatusconnector.h" #include <QLoggingCategory> #include <QNetworkReply> @@ -43,6 +46,7 @@ #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> +#include <QLoggingCategory> #include <qsslconfiguration.h> #include <qt5keychain/keychain.h> @@ -93,6 +97,7 @@ QString Account::davPath() const void Account::setSharedThis(AccountPtr sharedThis) { _sharedThis = sharedThis.toWeakRef(); + setupUserStatusConnector(); } QString Account::davPathBase() @@ -337,6 +342,24 @@ QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, return _am->sendCustomRequest(req, verb, data); } +QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, const QByteArray &data) +{ + req.setUrl(url); + req.setSslConfiguration(this->getOrCreateSslConfig()); + if (verb == "HEAD" && data.isEmpty()) { + return _am->head(req); + } else if (verb == "GET" && data.isEmpty()) { + return _am->get(req); + } else if (verb == "POST") { + return _am->post(req, data); + } else if (verb == "PUT") { + return _am->put(req, data); + } else if (verb == "DELETE" && data.isEmpty()) { + return _am->deleteResource(req); + } + return _am->sendCustomRequest(req, verb, data); +} + SimpleNetworkJob *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data) { auto job = new SimpleNetworkJob(sharedFromThis()); @@ -544,9 +567,18 @@ void Account::setCapabilities(const QVariantMap &caps) { _capabilities = Capabilities(caps); + setupUserStatusConnector(); trySetupPushNotifications(); } +void Account::setupUserStatusConnector() +{ + _userStatusConnector = std::make_shared<OcsUserStatusConnector>(sharedFromThis()); + connect(_userStatusConnector.get(), &UserStatusConnector::userStatusFetched, this, [this](const UserStatus &) { + emit userStatusChanged(); + }); +} + QString Account::serverVersion() const { return _serverVersion; @@ -744,4 +776,9 @@ PushNotifications *Account::pushNotifications() const return _pushNotifications; } +std::shared_ptr<UserStatusConnector> Account::userStatusConnector() const +{ + return _userStatusConnector; +} + } // namespace OCC diff --git a/src/libsync/account.h b/src/libsync/account.h index 00930b2b3..334425853 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -55,6 +55,7 @@ using AccountPtr = QSharedPointer<Account>; class AccessManager; class SimpleNetworkJob; class PushNotifications; +class UserStatusConnector; /** * @brief Reimplement this to handle SSL errors from libsync @@ -150,6 +151,9 @@ public: QNetworkRequest req = QNetworkRequest(), QIODevice *data = nullptr); + QNetworkReply *sendRawRequest(const QByteArray &verb, + const QUrl &url, QNetworkRequest req, const QByteArray &data); + /** Create and start network job for a simple one-off request. * * More complicated requests typically create their own job types. @@ -251,10 +255,13 @@ public: // Check for the directEditing capability void fetchDirectEditors(const QUrl &directEditingURL, const QString &directEditingETag); + void setupUserStatusConnector(); void trySetupPushNotifications(); PushNotifications *pushNotifications() const; void setPushNotificationsReconnectInterval(int interval); + std::shared_ptr<UserStatusConnector> userStatusConnector() const; + public slots: /// Used when forgetting credentials void clearQNAMCache(); @@ -287,6 +294,8 @@ signals: void pushNotificationsReady(Account *account); void pushNotificationsDisabled(Account *account); + void userStatusChanged(); + protected Q_SLOTS: void slotCredentialsFetched(); void slotCredentialsAsked(); @@ -343,6 +352,8 @@ private: PushNotifications *_pushNotifications = nullptr; + std::shared_ptr<UserStatusConnector> _userStatusConnector; + /* IMPORTANT - remove later - FIXME MS@2019-12-07 --> * TODO: For "Log out" & "Remove account": Remove client CA certs and KEY! * diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 04db298b1..9983821ae 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -187,13 +187,31 @@ bool Capabilities::chunkingNg() const return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0"; } -bool Capabilities::userStatus() const +bool Capabilities::userStatusNotification() const { return _capabilities.contains("notifications") && _capabilities["notifications"].toMap().contains("ocs-endpoints") && _capabilities["notifications"].toMap()["ocs-endpoints"].toStringList().contains("user-status"); } +bool Capabilities::userStatus() const +{ + if (!_capabilities.contains("user_status")) { + return false; + } + const auto userStatusMap = _capabilities["user_status"].toMap(); + return userStatusMap.value("enabled", false).toBool(); +} + +bool Capabilities::userStatusSupportsEmoji() const +{ + if (!userStatus()) { + return false; + } + const auto userStatusMap = _capabilities["user_status"].toMap(); + return userStatusMap.value("supports_emoji", false).toBool(); +} + PushNotificationTypes Capabilities::availablePushNotifications() const { if (!_capabilities.contains("notify_push")) { diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 078a0cd35..3040db890 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -58,7 +58,9 @@ public: bool sharePublicLinkMultiple() const; bool shareResharing() const; bool chunkingNg() const; + bool userStatusNotification() const; bool userStatus() const; + bool userStatusSupportsEmoji() const; /// Returns which kind of push notfications are available PushNotificationTypes availablePushNotifications() const; diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 4ea561da3..2a8b1a189 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -56,7 +56,8 @@ Q_LOGGING_CATEGORY(lcCse, "nextcloud.sync.clientsideencryption", QtInfoMsg) Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.e2e", QtInfoMsg) Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg) -QString baseUrl(){ +QString e2eeBaseUrl() +{ return QStringLiteral("ocs/v2.php/apps/end_to_end_encryption/api/v1/"); } @@ -1180,7 +1181,7 @@ void ClientSideEncryption::generateCSR(const AccountPtr &account, EVP_PKEY *keyP qCInfo(lcCse()) << "Returning the certificate"; qCInfo(lcCse()) << output; - auto job = new SignPublicKeyApiJob(account, baseUrl() + "public-key", this); + auto job = new SignPublicKeyApiJob(account, e2eeBaseUrl() + "public-key", this); job->setCsr(output); connect(job, &SignPublicKeyApiJob::jsonReceived, [this, account](const QJsonDocument& json, int retCode) { @@ -1212,7 +1213,7 @@ void ClientSideEncryption::encryptPrivateKey(const AccountPtr &account) auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(_privateKey), salt); // Send private key to the server - auto job = new StorePrivateKeyApiJob(account, baseUrl() + "private-key", this); + auto job = new StorePrivateKeyApiJob(account, e2eeBaseUrl() + "private-key", this); job->setPrivateKey(cryptedText); connect(job, &StorePrivateKeyApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { Q_UNUSED(doc); @@ -1296,7 +1297,7 @@ void ClientSideEncryption::decryptPrivateKey(const AccountPtr &account, const QB void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account) { qCInfo(lcCse()) << "Retrieving private key from server"; - auto job = new JsonApiJob(account, baseUrl() + "private-key", this); + auto job = new JsonApiJob(account, e2eeBaseUrl() + "private-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { if (retCode == 200) { QString key = doc.object()["ocs"].toObject()["data"].toObject()["private-key"].toString(); @@ -1315,7 +1316,7 @@ void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account) void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account) { qCInfo(lcCse()) << "Retrieving public key from server"; - auto job = new JsonApiJob(account, baseUrl() + "public-key", this); + auto job = new JsonApiJob(account, e2eeBaseUrl() + "public-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { if (retCode == 200) { QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[account->davUser()].toString(); @@ -1336,7 +1337,7 @@ void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account) void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr &account) { qCInfo(lcCse()) << "Retrieving public key from server"; - auto job = new JsonApiJob(account, baseUrl() + "server-key", this); + auto job = new JsonApiJob(account, e2eeBaseUrl() + "server-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { if (retCode == 200) { const auto serverPublicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-key"].toString().toLatin1(); diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index e89586451..751c7d4c8 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -23,7 +23,7 @@ class ReadPasswordJob; namespace OCC { -QString baseUrl(); +QString e2eeBaseUrl(); namespace EncryptionHelper { QByteArray generateRandomFilename(); diff --git a/src/libsync/clientsideencryptionjobs.cpp b/src/libsync/clientsideencryptionjobs.cpp index ad8e30c66..71bb8a510 100644 --- a/src/libsync/clientsideencryptionjobs.cpp +++ b/src/libsync/clientsideencryptionjobs.cpp @@ -27,7 +27,7 @@ namespace OCC { GetMetadataApiJob::GetMetadataApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId) +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId) { } @@ -63,7 +63,7 @@ StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray& b64Metadata, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _b64Metadata(b64Metadata) +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _b64Metadata(b64Metadata) { } @@ -104,8 +104,8 @@ UpdateMetadataApiJob::UpdateMetadataApiJob(const AccountPtr& account, const QByteArray& b64Metadata, const QByteArray& token, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), -_fileId(fileId), +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent) +, _fileId(fileId), _b64Metadata(b64Metadata), _token(token) { @@ -154,7 +154,7 @@ UnlockEncryptFolderApiJob::UnlockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray& token, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId), _token(token) +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId), _token(token) { } @@ -185,11 +185,10 @@ bool UnlockEncryptFolderApiJob::finished() } - DeleteMetadataApiJob::DeleteMetadataApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId) +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId) { } @@ -219,7 +218,7 @@ bool DeleteMetadataApiJob::finished() } LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId) +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId) { } @@ -258,7 +257,7 @@ bool LockEncryptFolderApiJob::finished() } SetEncryptionFlagApiJob::SetEncryptionFlagApiJob(const AccountPtr& account, const QByteArray& fileId, FlagAction flagAction, QObject* parent) -: AbstractNetworkJob(account, baseUrl() + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction) +: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction) { } diff --git a/src/libsync/datetimeprovider.cpp b/src/libsync/datetimeprovider.cpp new file mode 100644 index 000000000..084072312 --- /dev/null +++ b/src/libsync/datetimeprovider.cpp @@ -0,0 +1,17 @@ +#include "datetimeprovider.h" + +namespace OCC { + +DateTimeProvider::~DateTimeProvider() = default; + +QDateTime DateTimeProvider::currentDateTime() const +{ + return QDateTime::currentDateTime(); +} + +QDate DateTimeProvider::currentDate() const +{ + return QDate::currentDate(); +} + +} diff --git a/src/libsync/datetimeprovider.h b/src/libsync/datetimeprovider.h new file mode 100644 index 000000000..1525a2e3d --- /dev/null +++ b/src/libsync/datetimeprovider.h @@ -0,0 +1,18 @@ +#pragma once + +#include "owncloudlib.h" + +#include <QDateTime> + +namespace OCC { + +class OWNCLOUDSYNC_EXPORT DateTimeProvider +{ +public: + virtual ~DateTimeProvider(); + + virtual QDateTime currentDateTime() const; + + virtual QDate currentDate() const; +}; +} diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index 9224afc76..2800af0d6 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -830,13 +830,49 @@ void JsonApiJob::addRawHeader(const QByteArray &headerName, const QByteArray &va _request.setRawHeader(headerName, value); } +void JsonApiJob::setBody(const QJsonDocument &body) +{ + _body = body.toJson(); + qCDebug(lcJsonApiJob) << "Set body for request:" << _body; + if (!_body.isEmpty()) { + _request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + } +} + + +void JsonApiJob::setVerb(Verb value) +{ + _verb = value; +} + + +QByteArray JsonApiJob::verbToString() const +{ + switch (_verb) { + case Verb::Get: + return "GET"; + case Verb::Post: + return "POST"; + case Verb::Put: + return "PUT"; + case Verb::Delete: + return "DELETE"; + } + return "GET"; +} + void JsonApiJob::start() { addRawHeader("OCS-APIREQUEST", "true"); auto query = _additionalParams; query.addQueryItem(QLatin1String("format"), QLatin1String("json")); QUrl url = Utility::concatUrlPath(account()->url(), path(), query); - sendRequest(_usePOST ? "POST" : "GET", url, _request); + const auto httpVerb = verbToString(); + if (!_body.isEmpty()) { + sendRequest(httpVerb, url, _request, _body); + } else { + sendRequest(httpVerb, url, _request); + } AbstractNetworkJob::start(); } diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index 45fb12a22..01cfcdedd 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -22,6 +22,7 @@ #include <QBuffer> #include <QUrlQuery> +#include <QJsonDocument> #include <functional> class QUrl; @@ -375,6 +376,13 @@ class OWNCLOUDSYNC_EXPORT JsonApiJob : public AbstractNetworkJob { Q_OBJECT public: + enum class Verb { + Get, + Post, + Put, + Delete, + }; + explicit JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr); /** @@ -390,15 +398,9 @@ public: void addQueryParams(const QUrlQuery ¶ms); void addRawHeader(const QByteArray &headerName, const QByteArray &value); - /** - * @brief usePOST - allow job to do an anonymous POST request instead of GET - * @param params: (optional) true for POST, false for GET (default). - * - * This function needs to be called before start() obviously. - */ - void usePOST(bool usePOST = true) { - _usePOST = usePOST; - } + void setBody(const QJsonDocument &body); + + void setVerb(Verb value); public slots: void start() override; @@ -429,10 +431,13 @@ signals: void allowDesktopNotificationsChanged(bool isAllowed); private: + QByteArray _body; QUrlQuery _additionalParams; QNetworkRequest _request; - bool _usePOST = false; + Verb _verb = Verb::Get; + + QByteArray verbToString() const; }; /** diff --git a/src/libsync/ocsuserstatusconnector.cpp b/src/libsync/ocsuserstatusconnector.cpp new file mode 100644 index 000000000..95f3810e2 --- /dev/null +++ b/src/libsync/ocsuserstatusconnector.cpp @@ -0,0 +1,455 @@ +/* + * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "ocsuserstatusconnector.h" +#include "account.h" +#include "userstatusconnector.h" + +#include <networkjobs.h> + +#include <QDateTime> +#include <QtGlobal> +#include <QJsonDocument> +#include <QJsonValue> +#include <QLoggingCategory> +#include <QString> +#include <QJsonObject> +#include <QJsonArray> +#include <qdatetime.h> +#include <qjsonarray.h> +#include <qjsonobject.h> +#include <qloggingcategory.h> + +namespace { + +Q_LOGGING_CATEGORY(lcOcsUserStatusConnector, "nextcloud.gui.ocsuserstatusconnector", QtInfoMsg) + +OCC::UserStatus::OnlineStatus stringToUserOnlineStatus(const QString &status) +{ + // it needs to match the Status enum + const QHash<QString, OCC::UserStatus::OnlineStatus> preDefinedStatus { + { "online", OCC::UserStatus::OnlineStatus::Online }, + { "dnd", OCC::UserStatus::OnlineStatus::DoNotDisturb }, + { "away", OCC::UserStatus::OnlineStatus::Away }, + { "offline", OCC::UserStatus::OnlineStatus::Offline }, + { "invisible", OCC::UserStatus::OnlineStatus::Invisible } + }; + + // api should return invisible, dnd,... toLower() it is to make sure + // it matches _preDefinedStatus, otherwise the default is online (0) + return preDefinedStatus.value(status.toLower(), OCC::UserStatus::OnlineStatus::Online); +} + +QString onlineStatusToString(OCC::UserStatus::OnlineStatus status) +{ + switch (status) { + case OCC::UserStatus::OnlineStatus::Online: + return QStringLiteral("online"); + case OCC::UserStatus::OnlineStatus::DoNotDisturb: + return QStringLiteral("dnd"); + case OCC::UserStatus::OnlineStatus::Away: + return QStringLiteral("offline"); + case OCC::UserStatus::OnlineStatus::Offline: + return QStringLiteral("offline"); + case OCC::UserStatus::OnlineStatus::Invisible: + return QStringLiteral("invisible"); + } + return QStringLiteral("online"); +} + +OCC::Optional<OCC::ClearAt> jsonExtractClearAt(QJsonObject jsonObject) +{ + OCC::Optional<OCC::ClearAt> clearAt {}; + if (jsonObject.contains("clearAt") && !jsonObject.value("clearAt").isNull()) { + OCC::ClearAt clearAtValue; + clearAtValue._type = OCC::ClearAtType::Timestamp; + clearAtValue._timestamp = jsonObject.value("clearAt").toInt(); + clearAt = clearAtValue; + } + return clearAt; +} + +OCC::UserStatus jsonExtractUserStatus(QJsonObject json) +{ + const auto clearAt = jsonExtractClearAt(json); + + const OCC::UserStatus userStatus(json.value("messageId").toString(), + json.value("message").toString().trimmed(), + json.value("icon").toString().trimmed(), stringToUserOnlineStatus(json.value("status").toString()), + json.value("messageIsPredefined").toBool(false), clearAt); + + return userStatus; +} + +OCC::UserStatus jsonToUserStatus(const QJsonDocument &json) +{ + const QJsonObject defaultValues { + { "icon", "" }, + { "message", "" }, + { "status", "online" }, + { "messageIsPredefined", "false" }, + { "statusIsUserDefined", "false" } + }; + const auto retrievedData = json.object().value("ocs").toObject().value("data").toObject(defaultValues); + return jsonExtractUserStatus(retrievedData); +} + +quint64 clearAtEndOfToTimestamp(const OCC::ClearAt &clearAt) +{ + Q_ASSERT(clearAt._type == OCC::ClearAtType::EndOf); + + if (clearAt._endof == "day") { + return QDate::currentDate().addDays(1).startOfDay().toTime_t(); + } else if (clearAt._endof == "week") { + const auto days = Qt::Sunday - QDate::currentDate().dayOfWeek(); + return QDate::currentDate().addDays(days + 1).startOfDay().toTime_t(); + } + qCWarning(lcOcsUserStatusConnector) << "Can not handle clear at endof day type" << clearAt._endof; + return QDateTime::currentDateTime().toTime_t(); +} + +quint64 clearAtPeriodToTimestamp(const OCC::ClearAt &clearAt) +{ + return QDateTime::currentDateTime().addSecs(clearAt._period).toTime_t(); +} + +quint64 clearAtToTimestamp(const OCC::ClearAt &clearAt) +{ + switch (clearAt._type) { + case OCC::ClearAtType::Period: { + return clearAtPeriodToTimestamp(clearAt); + } + + case OCC::ClearAtType::EndOf: { + return clearAtEndOfToTimestamp(clearAt); + } + + case OCC::ClearAtType::Timestamp: { + return clearAt._timestamp; + } + } + + return 0; +} + +quint64 clearAtToTimestamp(const OCC::Optional<OCC::ClearAt> &clearAt) +{ + if (clearAt) { + return clearAtToTimestamp(*clearAt); + } + return 0; +} + +OCC::Optional<OCC::ClearAt> jsonToClearAt(QJsonObject jsonObject) +{ + OCC::Optional<OCC::ClearAt> clearAt; + + if (jsonObject.value("clearAt").isObject() && !jsonObject.value("clearAt").isNull()) { + OCC::ClearAt clearAtValue; + const auto clearAtObject = jsonObject.value("clearAt").toObject(); + const auto typeValue = clearAtObject.value("type").toString("period"); + if (typeValue == "period") { + const auto timeValue = clearAtObject.value("time").toInt(0); + clearAtValue._type = OCC::ClearAtType::Period; + clearAtValue._period = timeValue; + } else if (typeValue == "end-of") { + const auto timeValue = clearAtObject.value("time").toString("day"); + clearAtValue._type = OCC::ClearAtType::EndOf; + clearAtValue._endof = timeValue; + } else { + qCWarning(lcOcsUserStatusConnector) << "Can not handle clear type value" << typeValue; + } + clearAt = clearAtValue; + } + + return clearAt; +} + +OCC::UserStatus jsonToUserStatus(QJsonObject jsonObject) +{ + const auto clearAt = jsonToClearAt(jsonObject); + + OCC::UserStatus userStatus( + jsonObject.value("id").toString("no-id"), + jsonObject.value("message").toString("No message"), + jsonObject.value("icon").toString("no-icon"), + OCC::UserStatus::OnlineStatus::Online, + true, + clearAt); + + return userStatus; +} + +std::vector<OCC::UserStatus> jsonToPredefinedStatuses(QJsonArray jsonDataArray) +{ + std::vector<OCC::UserStatus> statuses; + for (const auto &jsonEntry : jsonDataArray) { + Q_ASSERT(jsonEntry.isObject()); + if (!jsonEntry.isObject()) { + continue; + } + statuses.push_back(jsonToUserStatus(jsonEntry.toObject())); + } + + return statuses; +} + + +const QString baseUrl("/ocs/v2.php/apps/user_status/api/v1"); +const QString userStatusBaseUrl = baseUrl + QStringLiteral("/user_status"); +} + +namespace OCC { + +OcsUserStatusConnector::OcsUserStatusConnector(AccountPtr account, QObject *parent) + : UserStatusConnector(parent) + , _account(account) +{ + Q_ASSERT(_account); + _userStatusSupported = _account->capabilities().userStatus(); + _userStatusEmojisSupported = _account->capabilities().userStatusSupportsEmoji(); +} + +void OcsUserStatusConnector::fetchUserStatus() +{ + qCDebug(lcOcsUserStatusConnector) << "Try to fetch user status"; + + if (!_userStatusSupported) { + qCDebug(lcOcsUserStatusConnector) << "User status not supported"; + emit error(Error::UserStatusNotSupported); + return; + } + + startFetchUserStatusJob(); +} + +void OcsUserStatusConnector::startFetchUserStatusJob() +{ + if (_getUserStatusJob) { + qCDebug(lcOcsUserStatusConnector) << "Get user status job is already running."; + return; + } + + _getUserStatusJob = new JsonApiJob(_account, userStatusBaseUrl, this); + connect(_getUserStatusJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusFetched); + _getUserStatusJob->start(); +} + +void OcsUserStatusConnector::onUserStatusFetched(const QJsonDocument &json, int statusCode) +{ + logResponse("user status fetched", json, statusCode); + + if (statusCode != 200) { + qCInfo(lcOcsUserStatusConnector) << "Slot fetch UserStatus finished with status code" << statusCode; + emit error(Error::CouldNotFetchUserStatus); + return; + } + + _userStatus = jsonToUserStatus(json); + emit userStatusFetched(_userStatus); +} + +void OcsUserStatusConnector::startFetchPredefinedStatuses() +{ + if (_getPredefinedStausesJob) { + qCDebug(lcOcsUserStatusConnector) << "Get predefined statuses job is already running"; + return; + } + + _getPredefinedStausesJob = new JsonApiJob(_account, + baseUrl + QStringLiteral("/predefined_statuses"), this); + connect(_getPredefinedStausesJob, &JsonApiJob::jsonReceived, this, + &OcsUserStatusConnector::onPredefinedStatusesFetched); + _getPredefinedStausesJob->start(); +} + +void OcsUserStatusConnector::fetchPredefinedStatuses() +{ + if (!_userStatusSupported) { + emit error(Error::UserStatusNotSupported); + return; + } + startFetchPredefinedStatuses(); +} + +void OcsUserStatusConnector::onPredefinedStatusesFetched(const QJsonDocument &json, int statusCode) +{ + logResponse("predefined statuses", json, statusCode); + + if (statusCode != 200) { + qCInfo(lcOcsUserStatusConnector) << "Slot predefined user statuses finished with status code" << statusCode; + emit error(Error::CouldNotFetchPredefinedUserStatuses); + return; + } + const auto jsonData = json.object().value("ocs").toObject().value("data"); + Q_ASSERT(jsonData.isArray()); + if (!jsonData.isArray()) { + return; + } + const auto statuses = jsonToPredefinedStatuses(jsonData.toArray()); + emit predefinedStatusesFetched(statuses); +} + +void OcsUserStatusConnector::logResponse(const QString &message, const QJsonDocument &json, int statusCode) +{ + qCDebug(lcOcsUserStatusConnector) << "Response from:" << message << "Status:" << statusCode << "Json:" << json; +} + +void OcsUserStatusConnector::setUserStatusOnlineStatus(UserStatus::OnlineStatus onlineStatus) +{ + _setOnlineStatusJob = new JsonApiJob(_account, + userStatusBaseUrl + QStringLiteral("/status"), this); + _setOnlineStatusJob->setVerb(JsonApiJob::Verb::Put); + // Set body + QJsonObject dataObject; + dataObject.insert("statusType", onlineStatusToString(onlineStatus)); + QJsonDocument body; + body.setObject(dataObject); + _setOnlineStatusJob->setBody(body); + connect(_setOnlineStatusJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusOnlineStatusSet); + _setOnlineStatusJob->start(); +} + +void OcsUserStatusConnector::setUserStatusMessagePredefined(const UserStatus &userStatus) +{ + Q_ASSERT(userStatus.messagePredefined()); + if (!userStatus.messagePredefined()) { + return; + } + + _setMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message/predefined"), this); + _setMessageJob->setVerb(JsonApiJob::Verb::Put); + // Set body + QJsonObject dataObject; + dataObject.insert("messageId", userStatus.id()); + if (userStatus.clearAt()) { + dataObject.insert("clearAt", static_cast<int>(clearAtToTimestamp(userStatus.clearAt()))); + } else { + dataObject.insert("clearAt", QJsonValue()); + } + QJsonDocument body; + body.setObject(dataObject); + _setMessageJob->setBody(body); + connect(_setMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusMessageSet); + _setMessageJob->start(); +} + +void OcsUserStatusConnector::setUserStatusMessageCustom(const UserStatus &userStatus) +{ + Q_ASSERT(!userStatus.messagePredefined()); + if (userStatus.messagePredefined()) { + return; + } + + if (!_userStatusEmojisSupported) { + emit error(Error::EmojisNotSupported); + return; + } + _setMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message/custom"), this); + _setMessageJob->setVerb(JsonApiJob::Verb::Put); + // Set body + QJsonObject dataObject; + dataObject.insert("statusIcon", userStatus.icon()); + dataObject.insert("message", userStatus.message()); + const auto clearAt = userStatus.clearAt(); + if (clearAt) { + dataObject.insert("clearAt", static_cast<int>(clearAtToTimestamp(*clearAt))); + } else { + dataObject.insert("clearAt", QJsonValue()); + } + QJsonDocument body; + body.setObject(dataObject); + _setMessageJob->setBody(body); + connect(_setMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusMessageSet); + _setMessageJob->start(); +} + +void OcsUserStatusConnector::setUserStatusMessage(const UserStatus &userStatus) +{ + if (userStatus.messagePredefined()) { + setUserStatusMessagePredefined(userStatus); + return; + } + setUserStatusMessageCustom(userStatus); +} + +void OcsUserStatusConnector::setUserStatus(const UserStatus &userStatus) +{ + if (!_userStatusSupported) { + emit error(Error::UserStatusNotSupported); + return; + } + + if (_setOnlineStatusJob || _setMessageJob) { + qCDebug(lcOcsUserStatusConnector) << "Set online status job or set message job are already running."; + return; + } + + setUserStatusOnlineStatus(userStatus.state()); + setUserStatusMessage(userStatus); +} + +void OcsUserStatusConnector::onUserStatusOnlineStatusSet(const QJsonDocument &json, int statusCode) +{ + logResponse("Online status set", json, statusCode); + + if (statusCode != 200) { + emit error(Error::CouldNotSetUserStatus); + return; + } +} + +void OcsUserStatusConnector::onUserStatusMessageSet(const QJsonDocument &json, int statusCode) +{ + logResponse("Message set", json, statusCode); + + if (statusCode != 200) { + emit error(Error::CouldNotSetUserStatus); + return; + } + + // We fetch the user status again because json does not contain + // the new message when user status was set from a predefined + // message + fetchUserStatus(); + + emit userStatusSet(); +} + +void OcsUserStatusConnector::clearMessage() +{ + _clearMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message")); + _clearMessageJob->setVerb(JsonApiJob::Verb::Delete); + connect(_clearMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onMessageCleared); + _clearMessageJob->start(); +} + +UserStatus OcsUserStatusConnector::userStatus() const +{ + return _userStatus; +} + +void OcsUserStatusConnector::onMessageCleared(const QJsonDocument &json, int statusCode) +{ + logResponse("Message cleared", json, statusCode); + + if (statusCode != 200) { + emit error(Error::CouldNotClearMessage); + return; + } + + _userStatus = {}; + emit messageCleared(); +} +} diff --git a/src/libsync/ocsuserstatusconnector.h b/src/libsync/ocsuserstatusconnector.h new file mode 100644 index 000000000..0d366419f --- /dev/null +++ b/src/libsync/ocsuserstatusconnector.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include "accountfwd.h" +#include "userstatusconnector.h" + +#include <QPointer> + +namespace OCC { + +class JsonApiJob; +class SimpleNetworkJob; + +class OWNCLOUDSYNC_EXPORT OcsUserStatusConnector : public UserStatusConnector +{ +public: + explicit OcsUserStatusConnector(AccountPtr account, QObject *parent = nullptr); + + void fetchUserStatus() override; + + void fetchPredefinedStatuses() override; + + void setUserStatus(const UserStatus &userStatus) override; + + void clearMessage() override; + + UserStatus userStatus() const override; + +private: + void onUserStatusFetched(const QJsonDocument &json, int statusCode); + void onPredefinedStatusesFetched(const QJsonDocument &json, int statusCode); + void onUserStatusOnlineStatusSet(const QJsonDocument &json, int statusCode); + void onUserStatusMessageSet(const QJsonDocument &json, int statusCode); + void onMessageCleared(const QJsonDocument &json, int statusCode); + + void logResponse(const QString &message, const QJsonDocument &json, int statusCode); + void startFetchUserStatusJob(); + void startFetchPredefinedStatuses(); + void setUserStatusOnlineStatus(UserStatus::OnlineStatus onlineStatus); + void setUserStatusMessage(const UserStatus &userStatus); + void setUserStatusMessagePredefined(const UserStatus &userStatus); + void setUserStatusMessageCustom(const UserStatus &userStatus); + + AccountPtr _account; + + bool _userStatusSupported = false; + bool _userStatusEmojisSupported = false; + + QPointer<JsonApiJob> _clearMessageJob {}; + QPointer<JsonApiJob> _setMessageJob {}; + QPointer<JsonApiJob> _setOnlineStatusJob {}; + QPointer<JsonApiJob> _getPredefinedStausesJob {}; + QPointer<JsonApiJob> _getUserStatusJob {}; + + UserStatus _userStatus; +}; +} diff --git a/src/libsync/userstatusconnector.cpp b/src/libsync/userstatusconnector.cpp new file mode 100644 index 000000000..178e8d794 --- /dev/null +++ b/src/libsync/userstatusconnector.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "userstatusconnector.h" +#include "theme.h" + +namespace OCC { + +UserStatus::UserStatus() = default; + +UserStatus::UserStatus( + const QString &id, const QString &message, const QString &icon, + OnlineStatus state, bool messagePredefined, const Optional<ClearAt> &clearAt) + : _id(id) + , _message(message) + , _icon(icon) + , _state(state) + , _messagePredefined(messagePredefined) + , _clearAt(clearAt) +{ +} + +QString UserStatus::id() const +{ + return _id; +} + +QString UserStatus::message() const +{ + return _message; +} + +QString UserStatus::icon() const +{ + return _icon; +} + +auto UserStatus::state() const -> OnlineStatus +{ + return _state; +} + +bool UserStatus::messagePredefined() const +{ + return _messagePredefined; +} + +QUrl UserStatus::stateIcon() const +{ + switch (_state) { + case UserStatus::OnlineStatus::Away: + return Theme::instance()->statusAwayImageSource(); + + case UserStatus::OnlineStatus::DoNotDisturb: + return Theme::instance()->statusDoNotDisturbImageSource(); + + case UserStatus::OnlineStatus::Invisible: + case UserStatus::OnlineStatus::Offline: + return Theme::instance()->statusInvisibleImageSource(); + + case UserStatus::OnlineStatus::Online: + return Theme::instance()->statusOnlineImageSource(); + } + + Q_UNREACHABLE(); +} + +Optional<ClearAt> UserStatus::clearAt() const +{ + return _clearAt; +} + +void UserStatus::setId(const QString &id) +{ + _id = id; +} + +void UserStatus::setMessage(const QString &message) +{ + _message = message; +} + +void UserStatus::setState(OnlineStatus state) +{ + _state = state; +} + +void UserStatus::setIcon(const QString &icon) +{ + _icon = icon; +} + +void UserStatus::setMessagePredefined(bool value) +{ + _messagePredefined = value; +} + +void UserStatus::setClearAt(const Optional<ClearAt> &dateTime) +{ + _clearAt = dateTime; +} + + +UserStatusConnector::UserStatusConnector(QObject *parent) + : QObject(parent) +{ +} + +UserStatusConnector::~UserStatusConnector() = default; +} diff --git a/src/libsync/userstatusconnector.h b/src/libsync/userstatusconnector.h new file mode 100644 index 000000000..d5593fe9e --- /dev/null +++ b/src/libsync/userstatusconnector.h @@ -0,0 +1,138 @@ +/* + * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include "common/result.h" +#include "owncloudlib.h" + +#include <QObject> +#include <QString> +#include <QMetaType> +#include <QUrl> +#include <QDateTime> +#include <QtGlobal> +#include <QVariant> + +#include <vector> + + +namespace OCC { + +enum class OWNCLOUDSYNC_EXPORT ClearAtType { + Period, + EndOf, + Timestamp +}; + +// TODO: If we can use C++17 make it a std::variant +struct OWNCLOUDSYNC_EXPORT ClearAt +{ + ClearAtType _type = ClearAtType::Period; + + quint64 _timestamp; + int _period; + QString _endof; +}; + +class OWNCLOUDSYNC_EXPORT UserStatus +{ + Q_GADGET + + Q_PROPERTY(QString id MEMBER _id) + Q_PROPERTY(QString message MEMBER _message) + Q_PROPERTY(QString icon MEMBER _icon) + Q_PROPERTY(OnlineStatus state MEMBER _state) + +public: + enum class OnlineStatus : quint8 { + Online, + DoNotDisturb, + Away, + Offline, + Invisible + }; + Q_ENUM(OnlineStatus); + + UserStatus(); + + UserStatus(const QString &id, const QString &message, const QString &icon, + OnlineStatus state, bool messagePredefined, const Optional<ClearAt> &clearAt = {}); + + Q_REQUIRED_RESULT QString id() const; + Q_REQUIRED_RESULT QString message() const; + Q_REQUIRED_RESULT QString icon() const; + Q_REQUIRED_RESULT OnlineStatus state() const; + Q_REQUIRED_RESULT Optional<ClearAt> clearAt() const; + + void setId(const QString &id); + void setMessage(const QString &message); + void setState(OnlineStatus state); + void setIcon(const QString &icon); + void setMessagePredefined(bool value); + void setClearAt(const Optional<ClearAt> &dateTime); + + Q_REQUIRED_RESULT bool messagePredefined() const; + + Q_REQUIRED_RESULT QUrl stateIcon() const; + +private: + QString _id; + QString _message; + QString _icon; + OnlineStatus _state = OnlineStatus::Online; + bool _messagePredefined; + Optional<ClearAt> _clearAt; +}; + +class OWNCLOUDSYNC_EXPORT UserStatusConnector : public QObject +{ + Q_OBJECT + +public: + enum class Error { + CouldNotFetchUserStatus, + CouldNotFetchPredefinedUserStatuses, + UserStatusNotSupported, + EmojisNotSupported, + CouldNotSetUserStatus, + CouldNotClearMessage + }; + Q_ENUM(Error) + + explicit UserStatusConnector(QObject *parent = nullptr); + + ~UserStatusConnector() override; + + virtual void fetchUserStatus() = 0; + + virtual void fetchPredefinedStatuses() = 0; + + virtual void setUserStatus(const UserStatus &userStatus) = 0; + + virtual void clearMessage() = 0; + + virtual UserStatus userStatus() const = 0; + +signals: + void userStatusFetched(const UserStatus &userStatus); + void predefinedStatusesFetched(const std::vector<UserStatus> &statuses); + void userStatusSet(); + void messageCleared(); + void error(Error error); +}; +} + +Q_DECLARE_METATYPE(OCC::UserStatusConnector *) +Q_DECLARE_METATYPE(OCC::UserStatus) |