From 61ef28634a15a8982907fe173debf22b8e3c5a95 Mon Sep 17 00:00:00 2001
From: alex-z <blackslayer4@gmail.com>
Date: Wed, 6 Dec 2023 17:53:15 +0100
Subject: [PATCH] Review comments. Part II.

Signed-off-by: alex-z <blackslayer4@gmail.com>
---
 src/libsync/CMakeLists.txt                    |   6 +
 src/libsync/account.cpp                       |   2 +-
 src/libsync/account.h                         |   2 +-
 src/libsync/clientsideencryption.cpp          |  18 +-
 src/libsync/clientstatusreporting.cpp         | 406 +-----------------
 src/libsync/clientstatusreporting.h           |  87 +---
 src/libsync/clientstatusreportingcommon.cpp   |  62 +++
 src/libsync/clientstatusreportingcommon.h     |  38 ++
 src/libsync/clientstatusreportingdatabase.cpp | 223 ++++++++++
 src/libsync/clientstatusreportingdatabase.h   |  68 +++
 src/libsync/clientstatusreportingnetwork.cpp  | 188 ++++++++
 src/libsync/clientstatusreportingnetwork.h    |  68 +++
 src/libsync/discovery.cpp                     |   4 +-
 src/libsync/owncloudpropagator.cpp            |  22 +-
 src/libsync/propagatedownload.cpp             |   4 +-
 src/libsync/syncengine.cpp                    |   6 +-
 src/libsync/vfs/cfapi/vfs_cfapi.cpp           |   2 +-
 test/testclientstatusreporting.cpp            |  48 ++-
 18 files changed, 739 insertions(+), 515 deletions(-)
 create mode 100644 src/libsync/clientstatusreportingcommon.cpp
 create mode 100644 src/libsync/clientstatusreportingcommon.h
 create mode 100644 src/libsync/clientstatusreportingdatabase.cpp
 create mode 100644 src/libsync/clientstatusreportingdatabase.h
 create mode 100644 src/libsync/clientstatusreportingnetwork.cpp
 create mode 100644 src/libsync/clientstatusreportingnetwork.h

diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt
index 0e0f85b65dda2..cd124bd3b653b 100644
--- a/src/libsync/CMakeLists.txt
+++ b/src/libsync/CMakeLists.txt
@@ -26,6 +26,12 @@ set(libsync_SRCS
     clientproxy.cpp
     clientstatusreporting.h
     clientstatusreporting.cpp
+    clientstatusreportingcommon.h
+    clientstatusreportingcommon.cpp
+    clientstatusreportingdatabase.h
+    clientstatusreportingdatabase.cpp
+    clientstatusreportingnetwork.h
+    clientstatusreportingnetwork.cpp
     clientstatusreportingrecord.h
     cookiejar.h
     cookiejar.cpp
diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp
index 7ceb196bdf0b2..bea76c53b9069 100644
--- a/src/libsync/account.cpp
+++ b/src/libsync/account.cpp
@@ -296,7 +296,7 @@ void Account::trySetupClientStatusReporting()
     }
 }
 
-void Account::reportClientStatus(const ClientStatusReporting::Status status) const
+void Account::reportClientStatus(const ClientStatusReportingStatus status) const
 {
     if (_clientStatusReporting) {
         _clientStatusReporting->reportClientStatus(status);
diff --git a/src/libsync/account.h b/src/libsync/account.h
index 005115adc0ff1..e118b53fb2f25 100644
--- a/src/libsync/account.h
+++ b/src/libsync/account.h
@@ -308,7 +308,7 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject
 
     void trySetupClientStatusReporting();
 
-    void reportClientStatus(const ClientStatusReporting::Status status) const;
+    void reportClientStatus(const ClientStatusReportingStatus status) const;
 
     [[nodiscard]] std::shared_ptr<UserStatusConnector> userStatusConnector() const;
 
diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp
index 71d19aed12e02..dd9263b024d62 100644
--- a/src/libsync/clientsideencryption.cpp
+++ b/src/libsync/clientsideencryption.cpp
@@ -1262,7 +1262,7 @@ bool ClientSideEncryption::sensitiveDataRemaining() const
 void ClientSideEncryption::failedToInitialize(const AccountPtr &account)
 {
     forgetSensitiveData(account);
-    account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+    account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
     Q_EMIT initializationFinished();
 }
 
@@ -1776,7 +1776,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
 
         if (metadataKeys.isEmpty()) {
             qCDebug(lcCse()) << "Could not migrate. No metadata keys found!";
-            _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+            _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
             return;
         }
 
@@ -1789,7 +1789,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
 
     if (_metadataKey.isEmpty()) {
         qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!";
-        _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+        _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
         return;
     }
 
@@ -1864,7 +1864,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
         } else {
             _metadataKey.clear();
             _files.clear();
-            _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+            _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
             return;
         }
     }
@@ -1903,7 +1903,7 @@ QByteArray FolderMetadata::decryptData(const QByteArray &data) const
     if (decryptResult.isEmpty())
     {
         qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key";
-        _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+        _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
         return {};
     }
     return QByteArray::fromBase64(decryptResult);
@@ -1921,7 +1921,7 @@ QByteArray FolderMetadata::decryptDataUsingKey(const QByteArray &data,
     if (decryptResult.isEmpty())
     {
         qCDebug(lcCse()) << "ERROR. Could not decrypt";
-        _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+        _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
         return {};
     }
 
@@ -1985,7 +1985,7 @@ QByteArray FolderMetadata::encryptedMetadata() const {
 
     if (_metadataKey.isEmpty()) {
         qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!";
-        _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+        _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
         return {};
     }
     const auto version = _account->capabilities().clientSideEncryptionVersion();
@@ -2007,7 +2007,7 @@ QByteArray FolderMetadata::encryptedMetadata() const {
 
         QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey);
         if (encryptedEncrypted.isEmpty()) {
-            _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+            _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
             qCDebug(lcCse) << "Metadata generation failed!";
         }
         QJsonObject file;
@@ -2095,7 +2095,7 @@ bool FolderMetadata::moveFromFileDropToFiles()
 
         if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) {
             qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key();
-            _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
+            _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
             continue;
         }
 
diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp
index 1930ce167f69b..0566995737282 100644
--- a/src/libsync/clientstatusreporting.cpp
+++ b/src/libsync/clientstatusreporting.cpp
@@ -14,423 +14,53 @@
 #include "clientstatusreporting.h"
 
 #include "account.h"
+#include "clientstatusreportingdatabase.h"
+#include "clientstatusreportingnetwork.h"
 #include "clientstatusreportingrecord.h"
-#include <configfile.h>
-#include "common/c_jhash.h"
-#include <networkjobs.h>
-
-namespace
-{
-constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime";
-constexpr auto statusNamesHash = "statusNamesHash";
-
-constexpr auto statusReportCategoryE2eErrors = "e2e_errors";
-constexpr auto statusReportCategoryProblems = "problems";
-constexpr auto statusReportCategorySyncConflicts = "sync_conflicts";
-constexpr auto statusReportCategoryVirus = "virus_detected";
-}
 
 namespace OCC
 {
 Q_LOGGING_CATEGORY(lcClientStatusReporting, "nextcloud.sync.clientstatusreporting", QtInfoMsg)
 
-ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent)
-    : QObject(parent)
-    , _account(account)
-{
-    init();
-}
-
-ClientStatusReporting::~ClientStatusReporting()
-{
-    if (_database.isOpen()) {
-        _database.close();
-    }
-}
-
-void ClientStatusReporting::init()
-{
-    Q_ASSERT(!_isInitialized);
-    if (_isInitialized) {
-        return;
-    }
-
-    for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) {
-        const auto statusString = statusStringFromNumber(static_cast<Status>(i));
-        _statusNamesAndHashes[i] = {statusString, c_jhash64((uint8_t *)statusString.data(), statusString.size(), 0)};
-    }
-
-    const auto dbPath = makeDbPath();
-    _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
-    _database.setDatabaseName(dbPath);
-
-    if (!_database.open()) {
-        qCDebug(lcClientStatusReporting) << "Could not setup client reporting, database connection error.";
-        return;
-    }
-
-    QSqlQuery query;
-    const auto prepareResult = query.prepare(QStringLiteral(
-        "CREATE TABLE IF NOT EXISTS clientstatusreporting("
-        "name VARCHAR(4096) PRIMARY KEY,"
-        "status INTEGER(8),"
-        "count INTEGER,"
-        "lastOccurrence INTEGER(8))"));
-    if (!prepareResult || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not setup client clientstatusreporting table:" << query.lastError().text();
-        return;
-    }
-
-    if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not setup client keyvalue table:" << query.lastError().text();
-        return;
-    }
-
-    updateStatusNamesHash();
-
-    _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval);
-    connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer);
-    _clientStatusReportingSendTimer.start();
-
-    _isInitialized = true;
-}
-
-QVector<ClientStatusReportingRecord> ClientStatusReporting::getClientStatusReportingRecords() const
+ClientStatusReporting::ClientStatusReporting(Account *account)
+    : _account(account)
 {
-    QVector<ClientStatusReportingRecord> records;
-
-    QMutexLocker locker(&_mutex);
-
-    QSqlQuery query;
-    if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << query.lastError().text();
-        return records;
+    for (int i = 0; i < ClientStatusReportingStatus::Count; ++i) {
+        const auto statusString = clientStatusstatusStringFromNumber(static_cast<ClientStatusReportingStatus>(i));
+        _statusStrings[i] = statusString;
     }
 
-    while (query.next()) {
-        ClientStatusReportingRecord record;
-        record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong();
-        record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray();
-        record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong();
-        record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong();
-        records.push_back(record);
-    }
-    return records;
-}
+    _database = QSharedPointer<ClientStatusReportingDatabase>::create(account);
+    _reporter = std::make_unique<ClientStatusReportingNetwork>(account, _database);
 
-void ClientStatusReporting::deleteClientStatusReportingRecords() const
-{
-    QSqlQuery query;
-    if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not delete records from clientstatusreporting:" << query.lastError().text();
-    }
+    _isInitialized = _database->isInitialized() && _reporter->isInitialized();
 }
 
-Result<void, QString> ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const
+ClientStatusReporting::~ClientStatusReporting()
 {
-    Q_ASSERT(record.isValid());
-    if (!record.isValid()) {
-        qCDebug(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord";
-        return {QStringLiteral("Invalid parameter")};
-    }
-
-    const auto recordCopy = record;
-
-    QMutexLocker locker(&_mutex);
-
-    QSqlQuery query;
-
-    const auto prepareResult = query.prepare(
-        QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) "
-        "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;"));
-    query.bindValue(QStringLiteral(":name"), recordCopy._name);
-    query.bindValue(QStringLiteral(":status"), recordCopy._status);
-    query.bindValue(QStringLiteral(":count"), 1);
-    query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence);
-
-    if (!prepareResult || !query.exec()) {
-        const auto errorMessage = query.lastError().text();
-        qCDebug(lcClientStatusReporting) << "Could not report client status:" << errorMessage;
-        return errorMessage;
-    }
-
-    return {};
+    // the sole purpose of this desrtuctor is to make unique_ptr work with forward declaration, but let's clearn the initialized flag too
+    _isInitialized = false;
 }
 
-void ClientStatusReporting::reportClientStatus(const Status status) const
+void ClientStatusReporting::reportClientStatus(const ClientStatusReportingStatus status) const
 {
     if (!_isInitialized) {
-        qCDebug(lcClientStatusReporting) << "Could not report status. Status reporting is not initialized";
         return;
     }
+
     Q_ASSERT(status >= 0 && status < Count);
-    if (status < 0 || status >= Status::Count) {
+    if (status < 0 || status >= ClientStatusReportingStatus::Count) {
         qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << status;
         return;
     }
 
     ClientStatusReportingRecord record;
-    record._name = _statusNamesAndHashes[status].first;
+    record._name = _statusStrings[status];
     record._status = status;
     record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch();
-    const auto result = setClientStatusReportingRecord(record);
+    const auto result = _database->setClientStatusReportingRecord(record);
     if (!result.isValid()) {
         qCDebug(lcClientStatusReporting) << "Could not report client status:" << result.error();
     }
 }
-
-void ClientStatusReporting::sendReportToServer()
-{
-    if (!_isInitialized) {
-        qCWarning(lcClientStatusReporting) << "Could not send report to server. Status reporting is not initialized";
-        return;
-    }
-
-    const auto lastSentReportTime = getLastSentReportTimestamp();
-    if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) {
-        return;
-    }
-
-    const auto report = prepareReport();
-    if (report.isEmpty()) {
-        return;
-    }
-
-    const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics"));
-    clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report));
-    clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put);
-    connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) {
-        if (statusCode == 0 || statusCode == 200 || statusCode == 201 || statusCode == 204) {
-            const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject();
-            const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt();
-            if (codeFromJson == 0 || codeFromJson == 200 || codeFromJson == 201 || codeFromJson == 204) {
-                reportToServerSentSuccessfully();
-                return;
-            }
-            qCDebug(lcClientStatusReporting) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson;
-        }
-    });
-    clientStatusReportingJob->start();
-}
-
-void ClientStatusReporting::reportToServerSentSuccessfully()
-{
-    deleteClientStatusReportingRecords();
-    setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch());
-}
-
-QString ClientStatusReporting::makeDbPath() const
-{
-    if (!dbPathForTesting.isEmpty()) {
-        return dbPathForTesting;
-    }
-    const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString());
-    const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5);
-
-    return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex()));
-}
-
-void ClientStatusReporting::updateStatusNamesHash()
-{
-    QByteArray statusNamesContatenated;
-    for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) {
-        statusNamesContatenated += statusStringFromNumber(static_cast<Status>(i));
-    }
-    statusNamesContatenated += QByteArray::number(ClientStatusReporting::Status::Count);
-    const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex();
-    const auto statusNamesHashFromDb = getStatusNamesHash();
-
-    if (statusNamesHashCurrent != statusNamesHashFromDb) {
-        deleteClientStatusReportingRecords();
-        setStatusNamesHash(statusNamesHashCurrent);
-    }
-}
-
-quint64 ClientStatusReporting::getLastSentReportTimestamp() const
-{
-    QMutexLocker locker(&_mutex);
-    QSqlQuery query;
-    const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)"));
-    query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp);
-    if (!prepareResult || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp;
-        return 0;
-    }
-    if (!query.next()) {
-        qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text();
-        return 0;
-    }
-    return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong();
-}
-
-void ClientStatusReporting::setStatusNamesHash(const QByteArray &hash) const
-{
-    QMutexLocker locker(&_mutex);
-    QSqlQuery query;
-    const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);"));
-    query.bindValue(QStringLiteral(":key"), statusNamesHash);
-    query.bindValue(QStringLiteral(":value"), hash);
-    if (!prepareResult || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not set status names hash.";
-        return;
-    }
-}
-
-QByteArray ClientStatusReporting::getStatusNamesHash() const
-{
-    QMutexLocker locker(&_mutex);
-    QSqlQuery query;
-    const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)"));
-    query.bindValue(QStringLiteral(":key"), statusNamesHash);
-    if (!prepareResult || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not get status names hash. No such record:" << statusNamesHash;
-        return {};
-    }
-    if (!query.next()) {
-        qCDebug(lcClientStatusReporting) << "Could not get status names hash:" << query.lastError().text();
-        return {};
-    }
-    return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray();
-}
-
-QVariantMap ClientStatusReporting::prepareReport() const
-{
-    const auto records = getClientStatusReportingRecords();
-    if (records.isEmpty()) {
-        return {};
-    }
-
-    QVariantMap report;
-    report[statusReportCategorySyncConflicts] = QVariantMap{};
-    report[statusReportCategoryProblems] = QVariantMap{};
-    report[statusReportCategoryVirus] = QVariantMap{};
-    report[statusReportCategoryE2eErrors] = QVariantMap{};
-
-    QVariantMap e2eeErrors;
-    QVariantMap problems;
-    QVariantMap syncConflicts;
-    QVariantMap virusDetectedErrors;
-
-    for (const auto &record : records) {
-        const auto categoryKey = classifyStatus(static_cast<Status>(record._status));
-
-        if (categoryKey.isEmpty()) {
-            qCDebug(lcClientStatusReporting) << "Could not classify status:";
-            continue;
-        }
-    
-        if (categoryKey == statusReportCategoryE2eErrors) {
-            const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt();
-            e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences;
-            e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence;
-            report[categoryKey] = e2eeErrors;
-        } else if (categoryKey == statusReportCategoryProblems) {
-            problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}};
-            report[categoryKey] = problems;
-        } else if (categoryKey == statusReportCategorySyncConflicts) {
-            const auto initialCount = syncConflicts[QStringLiteral("count")].toInt();
-            syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences;
-            syncConflicts[QStringLiteral("oldest")] = record._lastOccurence;
-            report[categoryKey] = syncConflicts;
-        } else if (categoryKey == statusReportCategoryVirus) {
-            const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt();
-            virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences;
-            virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence;
-            report[categoryKey] = virusDetectedErrors;
-        }
-    }
-    return report;
-}
-
-void ClientStatusReporting::setLastSentReportTimestamp(const quint64 timestamp) const
-{
-    QMutexLocker locker(&_mutex);
-    QSqlQuery query;
-    const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);"));
-    query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp);
-    query.bindValue(QStringLiteral(":value"), timestamp);
-    if (!prepareResult || !query.exec()) {
-        qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp;
-        return;
-    }
-}
-
-QByteArray ClientStatusReporting::statusStringFromNumber(const Status status)
-{
-    Q_ASSERT(status >= 0 && status < Count);
-    if (status < 0 || status >= Status::Count) {
-        qCDebug(lcClientStatusReporting) << "Invalid status:" << status;
-        return {};
-    }
-
-    switch (status) {
-    case DownloadError_Cannot_Create_File:
-        return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE");
-    case DownloadError_Conflict:
-        return QByteArrayLiteral("DownloadResult.CONFLICT");
-    case DownloadError_ConflictCaseClash:
-        return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH");
-    case DownloadError_ConflictInvalidCharacters:
-        return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS");
-    case DownloadError_No_Free_Space:
-        return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE");
-    case DownloadError_ServerError:
-        return QByteArrayLiteral("DownloadResult.SERVER_ERROR");
-    case DownloadError_Virtual_File_Hydration_Failure:
-        return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE");
-    case E2EeError_GeneralError:
-        return QByteArrayLiteral("E2EeError.General");
-    case UploadError_Conflict:
-        return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH");
-    case UploadError_ConflictInvalidCharacters:
-        return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS");
-    case UploadError_No_Free_Space:
-        return QByteArrayLiteral("UploadResult.NO_FREE_SPACE");
-    case UploadError_No_Write_Permissions:
-        return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS");
-    case UploadError_ServerError:
-        return QByteArrayLiteral("UploadResult.SERVER_ERROR");
-    case UploadError_Virus_Detected:
-        return QByteArrayLiteral("UploadResult.VIRUS_DETECTED");
-    case Count:
-        return {};
-    };
-    return {};
-}
-
-QByteArray ClientStatusReporting::classifyStatus(const Status status)
-{
-    Q_ASSERT(status >= 0 && status < Count);
-    if (status < 0 || status >= Status::Count) {
-        qCDebug(lcClientStatusReporting) << "Invalid status:" << status;
-        return {};
-    }
-
-    switch (status) {
-    case DownloadError_Conflict:
-    case DownloadError_ConflictCaseClash:
-    case DownloadError_ConflictInvalidCharacters:
-    case UploadError_Conflict:
-    case UploadError_ConflictInvalidCharacters:
-        return statusReportCategorySyncConflicts;
-    case DownloadError_Cannot_Create_File:
-    case DownloadError_No_Free_Space:
-    case DownloadError_ServerError:
-    case DownloadError_Virtual_File_Hydration_Failure:
-    case UploadError_No_Free_Space:
-    case UploadError_No_Write_Permissions:
-    case UploadError_ServerError:
-        return statusReportCategoryProblems;
-    case UploadError_Virus_Detected:
-        return statusReportCategoryVirus;
-    case E2EeError_GeneralError:
-        return statusReportCategoryE2eErrors;
-    case Count:
-        return {};
-    };
-    return {};
-}
-int ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes
-quint64 ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours
-QString ClientStatusReporting::dbPathForTesting;
 }
diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h
index 38198e18af893..669c4dc4fa1d4 100644
--- a/src/libsync/clientstatusreporting.h
+++ b/src/libsync/clientstatusreporting.h
@@ -15,101 +15,40 @@
 
 #include "owncloudlib.h"
 #include <common/result.h>
+#include "clientstatusreportingcommon.h"
+
+#include <memory>
 
-#include <QtCore/qglobal.h>
 #include <QtCore/qbytearray.h>
 #include <QtCore/qhash.h>
-#include <QtCore/qobject.h>
-#include <QtCore/qmutex.h>
-#include <QtCore/qpair.h>
-#include <QtCore/qstring.h>
-#include <QtCore/qtimer.h>
-#include <QtSql/qsqldatabase.h>
-#include <QtSql/qsqlerror.h>
-#include <QtSql/qsqlrecord.h>
-#include <QtSql/qsqlquery.h>
+#include <QtCore/qsharedpointer.h>
 
 namespace OCC {
 
 class Account;
+class ClientStatusReportingDatabase;
+class ClientStatusReportingNetwork;
 struct ClientStatusReportingRecord;
 
-class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject
+class OWNCLOUDSYNC_EXPORT ClientStatusReporting
 {
-    Q_OBJECT
 public:
-    enum Status {
-        DownloadError_Cannot_Create_File = 0,
-        DownloadError_Conflict,
-        DownloadError_ConflictCaseClash,
-        DownloadError_ConflictInvalidCharacters,
-        DownloadError_No_Free_Space,
-        DownloadError_ServerError,
-        DownloadError_Virtual_File_Hydration_Failure,
-        E2EeError_GeneralError,
-        UploadError_Conflict,
-        UploadError_ConflictInvalidCharacters,
-        UploadError_No_Free_Space,
-        UploadError_No_Write_Permissions,
-        UploadError_ServerError,
-        UploadError_Virus_Detected,
-        Count,
-    };
-    Q_ENUM(Status);
-
-    explicit ClientStatusReporting(Account *account, QObject *parent = nullptr);
-    ~ClientStatusReporting() override;
-
-    static QByteArray statusStringFromNumber(const Status status);
+    explicit ClientStatusReporting(Account *account);
+    ~ClientStatusReporting();
 
 private:
-    void init();
     // reporting must happen via Account
-    void reportClientStatus(const Status status) const;
-
-    [[nodiscard]] Result<void, QString> setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const;
-    [[nodiscard]] QVector<ClientStatusReportingRecord> getClientStatusReportingRecords() const;
-    void deleteClientStatusReportingRecords() const;
-
-    void setLastSentReportTimestamp(const quint64 timestamp) const;
-    [[nodiscard]] quint64 getLastSentReportTimestamp() const;
-
-    void setStatusNamesHash(const QByteArray &hash) const;
-    [[nodiscard]] QByteArray getStatusNamesHash() const;
-
-    [[nodiscard]] QVariantMap prepareReport() const;
-    void reportToServerSentSuccessfully();
-
-    [[nodiscard]] QString makeDbPath() const;
-
-    void updateStatusNamesHash();
-
-private slots:
-    void sendReportToServer();
-
-private:
-    static QByteArray classifyStatus(const Status status);
-
-public:
-    static int clientStatusReportingTrySendTimerInterval;
-    static quint64 repordSendIntervalMs;
-    // this must be set in unit tests on init
-    static QString dbPathForTesting;
-
-private:
+    void reportClientStatus(const ClientStatusReportingStatus status) const;
 
     Account *_account = nullptr;
 
-    QSqlDatabase _database;
-
     bool _isInitialized = false;
 
-    QTimer _clientStatusReportingSendTimer;
+    QHash<int, QByteArray> _statusStrings;
 
-    QHash<int, QPair<QByteArray, quint64>> _statusNamesAndHashes;
+    QSharedPointer<ClientStatusReportingDatabase> _database;
 
-    // inspired by SyncJournalDb
-    mutable QRecursiveMutex _mutex;
+    std::unique_ptr<ClientStatusReportingNetwork> _reporter;
 
     friend class Account;
 };
diff --git a/src/libsync/clientstatusreportingcommon.cpp b/src/libsync/clientstatusreportingcommon.cpp
new file mode 100644
index 0000000000000..62e48aa2d6669
--- /dev/null
+++ b/src/libsync/clientstatusreportingcommon.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@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 "clientstatusreportingcommon.h"
+#include <QDebug>
+
+namespace OCC {
+QByteArray clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status)
+{
+    Q_ASSERT(status >= 0 && status < Count);
+    if (status < 0 || status >= ClientStatusReportingStatus::Count) {
+        qDebug() << "Invalid status:" << status;
+        return {};
+    }
+
+    switch (status) {
+    case DownloadError_Cannot_Create_File:
+        return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE");
+    case DownloadError_Conflict:
+        return QByteArrayLiteral("DownloadResult.CONFLICT");
+    case DownloadError_ConflictCaseClash:
+        return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH");
+    case DownloadError_ConflictInvalidCharacters:
+        return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS");
+    case DownloadError_No_Free_Space:
+        return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE");
+    case DownloadError_ServerError:
+        return QByteArrayLiteral("DownloadResult.SERVER_ERROR");
+    case DownloadError_Virtual_File_Hydration_Failure:
+        return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE");
+    case E2EeError_GeneralError:
+        return QByteArrayLiteral("E2EeError.General");
+    case UploadError_Conflict:
+        return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH");
+    case UploadError_ConflictInvalidCharacters:
+        return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS");
+    case UploadError_No_Free_Space:
+        return QByteArrayLiteral("UploadResult.NO_FREE_SPACE");
+    case UploadError_No_Write_Permissions:
+        return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS");
+    case UploadError_ServerError:
+        return QByteArrayLiteral("UploadResult.SERVER_ERROR");
+    case UploadError_Virus_Detected:
+        return QByteArrayLiteral("UploadResult.VIRUS_DETECTED");
+    case Count:
+        return {};
+    };
+    return {};
+}
+}
diff --git a/src/libsync/clientstatusreportingcommon.h b/src/libsync/clientstatusreportingcommon.h
new file mode 100644
index 0000000000000..db18cd1580fa0
--- /dev/null
+++ b/src/libsync/clientstatusreportingcommon.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@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 "owncloudlib.h"
+#include <QtCore/qbytearray.h>
+
+namespace OCC {
+enum ClientStatusReportingStatus {
+    DownloadError_Cannot_Create_File = 0,
+    DownloadError_Conflict,
+    DownloadError_ConflictCaseClash,
+    DownloadError_ConflictInvalidCharacters,
+    DownloadError_No_Free_Space,
+    DownloadError_ServerError,
+    DownloadError_Virtual_File_Hydration_Failure,
+    E2EeError_GeneralError,
+    UploadError_Conflict,
+    UploadError_ConflictInvalidCharacters,
+    UploadError_No_Free_Space,
+    UploadError_No_Write_Permissions,
+    UploadError_ServerError,
+    UploadError_Virus_Detected,
+    Count,
+};
+QByteArray OWNCLOUDSYNC_EXPORT clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status);
+}
diff --git a/src/libsync/clientstatusreportingdatabase.cpp b/src/libsync/clientstatusreportingdatabase.cpp
new file mode 100644
index 0000000000000..489617730c8db
--- /dev/null
+++ b/src/libsync/clientstatusreportingdatabase.cpp
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@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 "clientstatusreportingdatabase.h"
+
+#include "account.h"
+#include <configfile.h>
+
+namespace
+{
+constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime";
+constexpr auto statusNamesHash = "statusNamesHash";
+}
+
+namespace OCC
+{
+Q_LOGGING_CATEGORY(lcClientStatusReportingDatabase, "nextcloud.sync.clientstatusreportingdatabase", QtInfoMsg)
+
+ClientStatusReportingDatabase::ClientStatusReportingDatabase(const Account *account)
+{
+    const auto dbPath = makeDbPath(account);
+    _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
+    _database.setDatabaseName(dbPath);
+
+    if (!_database.open()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not setup client reporting, database connection error.";
+        return;
+    }
+
+    QSqlQuery query;
+    const auto prepareResult =
+        query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS clientstatusreporting("
+                                     "name VARCHAR(4096) PRIMARY KEY,"
+                                     "status INTEGER(8),"
+                                     "count INTEGER,"
+                                     "lastOccurrence INTEGER(8))"));
+    if (!prepareResult || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not setup client clientstatusreporting table:" << query.lastError().text();
+        return;
+    }
+
+    if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not setup client keyvalue table:" << query.lastError().text();
+        return;
+    }
+
+    updateStatusNamesHash();
+
+    _isInitialized = true;
+}
+
+ClientStatusReportingDatabase::~ClientStatusReportingDatabase()
+{
+    if (_database.isOpen()) {
+        _database.close();
+    }
+}
+
+QVector<ClientStatusReportingRecord> ClientStatusReportingDatabase::getClientStatusReportingRecords() const
+{
+    QVector<ClientStatusReportingRecord> records;
+
+    QMutexLocker locker(&_mutex);
+
+    QSqlQuery query;
+    if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not get records from clientstatusreporting:" << query.lastError().text();
+        return records;
+    }
+
+    while (query.next()) {
+        ClientStatusReportingRecord record;
+        record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong();
+        record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray();
+        record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong();
+        record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong();
+        records.push_back(record);
+    }
+    return records;
+}
+
+void ClientStatusReportingDatabase::deleteClientStatusReportingRecords() const
+{
+    QSqlQuery query;
+    if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not delete records from clientstatusreporting:" << query.lastError().text();
+    }
+}
+
+Result<void, QString> ClientStatusReportingDatabase::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const
+{
+    Q_ASSERT(record.isValid());
+    if (!record.isValid()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Failed to set ClientStatusReportingRecord";
+        return {QStringLiteral("Invalid parameter")};
+    }
+
+    const auto recordCopy = record;
+
+    QMutexLocker locker(&_mutex);
+
+    QSqlQuery query;
+
+    const auto prepareResult = query.prepare(
+        QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) "
+        "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;"));
+    query.bindValue(QStringLiteral(":name"), recordCopy._name);
+    query.bindValue(QStringLiteral(":status"), recordCopy._status);
+    query.bindValue(QStringLiteral(":count"), 1);
+    query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence);
+
+    if (!prepareResult || !query.exec()) {
+        const auto errorMessage = query.lastError().text();
+        qCDebug(lcClientStatusReportingDatabase) << "Could not report client status:" << errorMessage;
+        return errorMessage;
+    }
+
+    return {};
+}
+
+QString ClientStatusReportingDatabase::makeDbPath(const Account *account) const
+{
+    if (!dbPathForTesting.isEmpty()) {
+        return dbPathForTesting;
+    }
+    const auto databaseId = QStringLiteral("%1@%2").arg(account->davUser(), account->url().toString());
+    const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5);
+
+    return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex()));
+}
+
+void ClientStatusReportingDatabase::updateStatusNamesHash()
+{
+    QByteArray statusNamesContatenated;
+    for (int i = 0; i < ClientStatusReportingStatus::Count; ++i) {
+        statusNamesContatenated += clientStatusstatusStringFromNumber(static_cast<ClientStatusReportingStatus>(i));
+    }
+    statusNamesContatenated += QByteArray::number(ClientStatusReportingStatus::Count);
+    const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex();
+    const auto statusNamesHashFromDb = getStatusNamesHash();
+
+    if (statusNamesHashCurrent != statusNamesHashFromDb) {
+        deleteClientStatusReportingRecords();
+        setStatusNamesHash(statusNamesHashCurrent);
+    }
+}
+
+quint64 ClientStatusReportingDatabase::getLastSentReportTimestamp() const
+{
+    QMutexLocker locker(&_mutex);
+    QSqlQuery query;
+    const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)"));
+    query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp);
+    if (!prepareResult || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp;
+        return 0;
+    }
+    if (!query.next()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text();
+        return 0;
+    }
+    return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong();
+}
+
+void ClientStatusReportingDatabase::setStatusNamesHash(const QByteArray &hash) const
+{
+    QMutexLocker locker(&_mutex);
+    QSqlQuery query;
+    const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);"));
+    query.bindValue(QStringLiteral(":key"), statusNamesHash);
+    query.bindValue(QStringLiteral(":value"), hash);
+    if (!prepareResult || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not set status names hash.";
+        return;
+    }
+}
+
+QByteArray ClientStatusReportingDatabase::getStatusNamesHash() const
+{
+    QMutexLocker locker(&_mutex);
+    QSqlQuery query;
+    const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)"));
+    query.bindValue(QStringLiteral(":key"), statusNamesHash);
+    if (!prepareResult || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not get status names hash. No such record:" << statusNamesHash;
+        return {};
+    }
+    if (!query.next()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not get status names hash:" << query.lastError().text();
+        return {};
+    }
+    return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray();
+}
+
+bool ClientStatusReportingDatabase::isInitialized() const
+{
+    return _isInitialized;
+}
+
+void ClientStatusReportingDatabase::setLastSentReportTimestamp(const quint64 timestamp) const
+{
+    QMutexLocker locker(&_mutex);
+    QSqlQuery query;
+    const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);"));
+    query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp);
+    query.bindValue(QStringLiteral(":value"), timestamp);
+    if (!prepareResult || !query.exec()) {
+        qCDebug(lcClientStatusReportingDatabase) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp;
+        return;
+    }
+}
+QString ClientStatusReportingDatabase::dbPathForTesting;
+}
diff --git a/src/libsync/clientstatusreportingdatabase.h b/src/libsync/clientstatusreportingdatabase.h
new file mode 100644
index 0000000000000..fd1347c1a7079
--- /dev/null
+++ b/src/libsync/clientstatusreportingdatabase.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@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 "owncloudlib.h"
+#include <common/result.h>
+#include "clientstatusreportingcommon.h"
+#include "clientstatusreportingrecord.h"
+
+#include <QtCore/qglobal.h>
+#include <QtCore/qbytearray.h>
+#include <QtCore/qmutex.h>
+#include <QtCore/qstring.h>
+#include <QtSql/qsqldatabase.h>
+#include <QtSql/qsqlerror.h>
+#include <QtSql/qsqlrecord.h>
+#include <QtSql/qsqlquery.h>
+
+namespace OCC {
+
+class Account;
+
+class OWNCLOUDSYNC_EXPORT ClientStatusReportingDatabase
+{
+public:
+    explicit ClientStatusReportingDatabase(const Account *account);
+    ~ClientStatusReportingDatabase();
+
+    [[nodiscard]] Result<void, QString> setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const;
+    [[nodiscard]] QVector<ClientStatusReportingRecord> getClientStatusReportingRecords() const;
+    void deleteClientStatusReportingRecords() const;
+
+    void setLastSentReportTimestamp(const quint64 timestamp) const;
+    [[nodiscard]] quint64 getLastSentReportTimestamp() const;
+
+    void setStatusNamesHash(const QByteArray &hash) const;
+    [[nodiscard]] QByteArray getStatusNamesHash() const;
+
+    [[nodiscard]] bool isInitialized() const;
+
+private:
+    [[nodiscard]] QString makeDbPath(const Account *account) const;
+    void updateStatusNamesHash();
+
+public:
+    // this must be set in unit tests on init
+    static QString dbPathForTesting;
+
+private:
+    QSqlDatabase _database;
+
+    bool _isInitialized = false;
+
+    // inspired by SyncJournalDb
+    mutable QRecursiveMutex _mutex;
+};
+}
diff --git a/src/libsync/clientstatusreportingnetwork.cpp b/src/libsync/clientstatusreportingnetwork.cpp
new file mode 100644
index 0000000000000..cc871fe8f8784
--- /dev/null
+++ b/src/libsync/clientstatusreportingnetwork.cpp
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@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 "clientstatusreportingnetwork.h"
+
+#include "account.h"
+#include "clientstatusreportingdatabase.h"
+#include "clientstatusreportingrecord.h"
+#include <networkjobs.h>
+
+namespace
+{
+constexpr auto statusReportCategoryE2eErrors = "e2e_errors";
+constexpr auto statusReportCategoryProblems = "problems";
+constexpr auto statusReportCategorySyncConflicts = "sync_conflicts";
+constexpr auto statusReportCategoryVirus = "virus_detected";
+}
+
+namespace OCC
+{
+Q_LOGGING_CATEGORY(lcClientStatusReportingNetwork, "nextcloud.sync.clientstatusreportingnetwork", QtInfoMsg)
+
+ClientStatusReportingNetwork::ClientStatusReportingNetwork(Account *account, const QSharedPointer<ClientStatusReportingDatabase> database, QObject *parent)
+    : QObject(parent)
+    , _account(account)
+    , _database(database)
+{
+    init();
+}
+
+ClientStatusReportingNetwork::~ClientStatusReportingNetwork()
+{
+}
+
+void ClientStatusReportingNetwork::init()
+{
+    Q_ASSERT(!_isInitialized);
+    if (_isInitialized) {
+        return;
+    }
+
+    _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval);
+    connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReportingNetwork::sendReportToServer);
+    _clientStatusReportingSendTimer.start();
+
+    _isInitialized = true;
+}
+
+bool ClientStatusReportingNetwork::isInitialized() const
+{
+    return _isInitialized;
+}
+
+void ClientStatusReportingNetwork::sendReportToServer()
+{
+    if (!_isInitialized) {
+        qCWarning(lcClientStatusReportingNetwork) << "Could not send report to server. Status reporting is not initialized";
+        return;
+    }
+
+    const auto lastSentReportTime = _database->getLastSentReportTimestamp();
+    if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) {
+        return;
+    }
+
+    const auto report = prepareReport();
+    if (report.isEmpty()) {
+        return;
+    }
+
+    const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics"));
+    clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report));
+    clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put);
+    connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) {
+        if (statusCode == 0 || statusCode == 200 || statusCode == 201 || statusCode == 204) {
+            const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject();
+            const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt();
+            if (codeFromJson == 0 || codeFromJson == 200 || codeFromJson == 201 || codeFromJson == 204) {
+                reportToServerSentSuccessfully();
+                return;
+            }
+            qCDebug(lcClientStatusReportingNetwork) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson;
+        }
+    });
+    clientStatusReportingJob->start();
+}
+
+void ClientStatusReportingNetwork::reportToServerSentSuccessfully()
+{
+    _database->deleteClientStatusReportingRecords();
+    _database->setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch());
+}
+
+QVariantMap ClientStatusReportingNetwork::prepareReport() const
+{
+    const auto records = _database->getClientStatusReportingRecords();
+    if (records.isEmpty()) {
+        return {};
+    }
+
+    QVariantMap report;
+    report[statusReportCategorySyncConflicts] = QVariantMap{};
+    report[statusReportCategoryProblems] = QVariantMap{};
+    report[statusReportCategoryVirus] = QVariantMap{};
+    report[statusReportCategoryE2eErrors] = QVariantMap{};
+
+    QVariantMap e2eeErrors;
+    QVariantMap problems;
+    QVariantMap syncConflicts;
+    QVariantMap virusDetectedErrors;
+
+    for (const auto &record : records) {
+        const auto categoryKey = classifyStatus(static_cast<ClientStatusReportingStatus>(record._status));
+
+        if (categoryKey.isEmpty()) {
+            qCDebug(lcClientStatusReportingNetwork) << "Could not classify status:";
+            continue;
+        }
+    
+        if (categoryKey == statusReportCategoryE2eErrors) {
+            const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt();
+            e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences;
+            e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence;
+            report[categoryKey] = e2eeErrors;
+        } else if (categoryKey == statusReportCategoryProblems) {
+            problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}};
+            report[categoryKey] = problems;
+        } else if (categoryKey == statusReportCategorySyncConflicts) {
+            const auto initialCount = syncConflicts[QStringLiteral("count")].toInt();
+            syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences;
+            syncConflicts[QStringLiteral("oldest")] = record._lastOccurence;
+            report[categoryKey] = syncConflicts;
+        } else if (categoryKey == statusReportCategoryVirus) {
+            const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt();
+            virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences;
+            virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence;
+            report[categoryKey] = virusDetectedErrors;
+        }
+    }
+    return report;
+}
+
+QByteArray ClientStatusReportingNetwork::classifyStatus(const ClientStatusReportingStatus status)
+{
+    Q_ASSERT(status >= 0 && status < Count);
+    if (status < 0 || status >= ClientStatusReportingStatus::Count) {
+        qCDebug(lcClientStatusReportingNetwork) << "Invalid status:" << status;
+        return {};
+    }
+
+    switch (status) {
+    case DownloadError_Conflict:
+    case DownloadError_ConflictCaseClash:
+    case DownloadError_ConflictInvalidCharacters:
+    case UploadError_Conflict:
+    case UploadError_ConflictInvalidCharacters:
+        return statusReportCategorySyncConflicts;
+    case DownloadError_Cannot_Create_File:
+    case DownloadError_No_Free_Space:
+    case DownloadError_ServerError:
+    case DownloadError_Virtual_File_Hydration_Failure:
+    case UploadError_No_Free_Space:
+    case UploadError_No_Write_Permissions:
+    case UploadError_ServerError:
+        return statusReportCategoryProblems;
+    case UploadError_Virus_Detected:
+        return statusReportCategoryVirus;
+    case E2EeError_GeneralError:
+        return statusReportCategoryE2eErrors;
+    case Count:
+        return {};
+    };
+    return {};
+}
+
+int ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes
+quint64 ClientStatusReportingNetwork::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours
+}
diff --git a/src/libsync/clientstatusreportingnetwork.h b/src/libsync/clientstatusreportingnetwork.h
new file mode 100644
index 0000000000000..e41bc5859ff0f
--- /dev/null
+++ b/src/libsync/clientstatusreportingnetwork.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@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 "owncloudlib.h"
+#include <common/result.h>
+#include "clientstatusreportingcommon.h"
+
+#include <QtCore/qglobal.h>
+#include <QtCore/qbytearray.h>
+#include <QtCore/qobject.h>
+#include <QtCore/qsharedpointer.h>
+#include <QtCore/qstring.h>
+#include <QtCore/qtimer.h>
+
+namespace OCC {
+
+class Account;
+class ClientStatusReportingDatabase;
+struct ClientStatusReportingRecord;
+
+class OWNCLOUDSYNC_EXPORT ClientStatusReportingNetwork : public QObject
+{
+    Q_OBJECT
+public:
+    explicit ClientStatusReportingNetwork(Account *account, const QSharedPointer<ClientStatusReportingDatabase> database, QObject *parent = nullptr);
+    ~ClientStatusReportingNetwork() override;
+
+private:
+    void init();
+
+    [[nodiscard]] QVariantMap prepareReport() const;
+    void reportToServerSentSuccessfully();
+
+private slots:
+    void sendReportToServer();
+
+public:
+    [[nodiscard]] bool isInitialized() const;
+
+    static QByteArray classifyStatus(const ClientStatusReportingStatus status);
+
+    static int clientStatusReportingTrySendTimerInterval;
+    static quint64 repordSendIntervalMs;
+    // this must be set in unit tests on init
+    static QString dbPathForTesting;
+
+private:
+    Account *_account = nullptr;
+
+    QSharedPointer<ClientStatusReportingDatabase> _database;
+
+    bool _isInitialized = false;
+
+    QTimer _clientStatusReportingSendTimer;
+};
+}
diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp
index c316dd518aa40..dc56ec602b85e 100644
--- a/src/libsync/discovery.cpp
+++ b/src/libsync/discovery.cpp
@@ -1709,13 +1709,13 @@ bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item)
             // No permissions set
             return true;
         } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
-            _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions);
+            _discoveryData->_account->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Write_Permissions);
             qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
             item->_instruction = CSYNC_INSTRUCTION_ERROR;
             item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder");
             return false;
         } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) {
-            _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions);
+            _discoveryData->_account->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Write_Permissions);
             qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
             item->_instruction = CSYNC_INSTRUCTION_ERROR;
             item->_errorString = tr("Not allowed because you don't have permission to add files in that folder");
diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp
index 39b42f7518589..e2628d48a1b99 100644
--- a/src/libsync/owncloudpropagator.cpp
+++ b/src/libsync/owncloudpropagator.cpp
@@ -342,20 +342,20 @@ void PropagateItemJob::reportClientStatuses()
 {
     if (_item->_status == SyncFileItem::Status::Conflict) {
         if (_item->_direction == SyncFileItem::Direction::Up) {
-            propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict);
+            propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_Conflict);
         } else {
-            propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict);
+            propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict);
         }
     } else if (_item->_status == SyncFileItem::Status::FileNameClash) {
         if (_item->_direction == SyncFileItem::Direction::Up) {
-            propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters);
+            propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters);
         } else {
-            propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters);
+            propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters);
         }
     } else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) {
-        propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters);
+        propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters);
     } else if (_item->_status == SyncFileItem::Status::FileNameInvalid) {
-        propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters);
+        propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters);
     } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200 && _item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) {
         if (_item->_direction == SyncFileItem::Up) {
             const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == 400 || _item->_httpErrorCode == 415);
@@ -363,12 +363,12 @@ void PropagateItemJob::reportClientStatuses()
             if (isCodeBadReqOrUnsupportedMediaType && isExceptionInfoPresent
                 && _item->_errorExceptionName.contains(QStringLiteral("UnsupportedMediaType"))
                 && _item->_errorExceptionMessage.contains(QStringLiteral("virus"), Qt::CaseInsensitive)) {
-                propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Virus_Detected);
+                propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_Virus_Detected);
             } else {
-                propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError);
+                propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ServerError);
             }
         } else {
-            propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError);
+            propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ServerError);
         }
     }
 }
@@ -954,7 +954,7 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item,
     }
 
     _journal->setConflictRecord(conflictRecord);
-    account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict);
+    account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict);
 
     // Create a new upload job if the new conflict file should be uploaded
     if (account()->capabilities().uploadConflictFiles()) {
@@ -1027,7 +1027,7 @@ OCC::Optional<QString> OwncloudPropagator::createCaseClashConflict(const SyncFil
     }
 
     _journal->setCaseConflictRecord(conflictRecord);
-    account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictCaseClash);
+    account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictCaseClash);
 
     // Need a new sync to detect the created copy of the conflicting file
     _anotherSyncNeeded = true;
diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp
index b78adaae1c46c..afdf33d491b42 100644
--- a/src/libsync/propagatedownload.cpp
+++ b/src/libsync/propagatedownload.cpp
@@ -675,7 +675,7 @@ void PropagateDownloadFile::startDownload()
     if (_tmpFile.exists())
         FileSystem::setFileReadOnly(_tmpFile.fileName(), false);
     if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) {
-        propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File);
+        propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Cannot_Create_File);
         qCWarning(lcPropagateDownload) << "could not open temporary file" << _tmpFile.fileName();
         done(SyncFileItem::NormalError, _tmpFile.errorString(), ErrorCategory::GenericError);
         return;
@@ -1260,7 +1260,7 @@ void PropagateDownloadFile::downloadFinished()
     emit propagator()->touchedFile(filename);
     // The fileChanged() check is done above to generate better error messages.
     if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) {
-        propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File);
+        propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Cannot_Create_File);
         qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(filename);
         // If the file is locked, we want to retry this sync when it
         // becomes available again, otherwise try again directly
diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp
index ed2e09d028dd9..523f3506a193b 100644
--- a/src/libsync/syncengine.cpp
+++ b/src/libsync/syncengine.cpp
@@ -314,7 +314,7 @@ void SyncEngine::conflictRecordMaintenance()
             }
 
             _journal->setConflictRecord(record);
-            account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict);
+            account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict);
         }
     }
 }
@@ -1262,7 +1262,7 @@ void SyncEngine::slotSummaryError(const QString &message)
 
 void SyncEngine::slotInsufficientLocalStorage()
 {
-    account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space);
+    account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_No_Free_Space);
     slotSummaryError(
         tr("Disk space is low: Downloads that would reduce free space "
            "below %1 were skipped.")
@@ -1271,7 +1271,7 @@ void SyncEngine::slotInsufficientLocalStorage()
 
 void SyncEngine::slotInsufficientRemoteStorage()
 {
-    account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space);
+    account()->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Free_Space);
     auto msg = tr("There is insufficient space available on the server for some uploads.");
     if (_uniqueErrors.contains(msg))
         return;
diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp
index 4a5a958eeabc5..935cd8605fe4f 100644
--- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp
+++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp
@@ -464,7 +464,7 @@ void VfsCfApi::onHydrationJobFinished(HydrationJob *job)
     qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status();
     emit hydrationRequestFinished(job->requestId());
     if (!job->errorString().isEmpty()) {
-        params().account->reportClientStatus(ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure);
+        params().account->reportClientStatus(ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure);
         emit failureHydrating(job->errorCode(), job->statusCode(), job->errorString(), job->folderPath());
     }
 }
diff --git a/test/testclientstatusreporting.cpp b/test/testclientstatusreporting.cpp
index c796b7a5a75c0..e0f625ae00876 100644
--- a/test/testclientstatusreporting.cpp
+++ b/test/testclientstatusreporting.cpp
@@ -13,7 +13,9 @@
  */
 #include "account.h"
 #include "accountstate.h"
-#include "clientstatusreporting.h"
+#include "clientstatusreportingcommon.h"
+#include "clientstatusreportingdatabase.h"
+#include "clientstatusreportingnetwork.h"
 #include "syncenginetestutils.h"
 
 #include <QSignalSpy>
@@ -39,8 +41,8 @@ class TestClientStatusReporting : public QObject
 private slots:
     void initTestCase()
     {
-        OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000;
-        OCC::ClientStatusReporting::repordSendIntervalMs = 2000;
+        OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval = 1000;
+        OCC::ClientStatusReportingNetwork::repordSendIntervalMs = 2000;
 
         fakeQnam.reset(new FakeQNAM({}));
         account = OCC::Account::create().get();
@@ -53,7 +55,7 @@ private slots:
         const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5);
         dbFilePath = QDir::tempPath() + QStringLiteral("/.tests_userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex()));
         QFile(dbFilePath).remove();
-        OCC::ClientStatusReporting::dbPathForTesting = dbFilePath;
+        OCC::ClientStatusReportingDatabase::dbPathForTesting = dbFilePath;
 
         QVariantMap capabilities;
         capabilities[QStringLiteral("security_guard")] = QVariantMap{
@@ -74,30 +76,30 @@ private slots:
     {
         for (int i = 0; i < 2; ++i) {
             // 5 conflicts
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Conflict);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Conflict);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictCaseClash);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Conflict);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_Conflict);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ConflictCaseClash);
 
             // 4 problems
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ServerError);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ServerError);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ServerError);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ServerError);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure);
             // 3 occurances of UploadError_No_Write_Permissions
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions);
 
             // 3 occurances of UploadError_Virus_Detected
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected);
 
             // 2 occurances of E2EeError_GeneralError
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
-            account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError);
-            QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
+            account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
+            QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs);
 
             QVERIFY(!bodyReceivedAndParsed.isEmpty());
 
@@ -120,7 +122,7 @@ private slots:
             const auto problemsReceived = bodyReceivedAndParsed.value("problems").toMap();
             QVERIFY(!problemsReceived.isEmpty());
             QCOMPARE(problemsReceived.size(), 4);
-            const auto problemsNoWritePermissions = problemsReceived.value(OCC::ClientStatusReporting::statusStringFromNumber(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions)).toMap();
+            const auto problemsNoWritePermissions = problemsReceived.value(OCC::clientStatusstatusStringFromNumber(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions)).toMap();
             // among those, 3 occurances of UploadError_No_Write_Permissions
             QCOMPARE(problemsNoWritePermissions.value("count"), 3);
 
@@ -130,7 +132,7 @@ private slots:
 
     void testNothingReportedAndNothingSent()
     {
-        QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs);
+        QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs);
         QVERIFY(bodyReceivedAndParsed.isEmpty());
     }