diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/avatar.cpp | 12 | ||||
-rw-r--r-- | lib/connection.cpp | 133 | ||||
-rw-r--r-- | lib/connection.h | 18 | ||||
-rw-r--r-- | lib/converters.h | 2 | ||||
-rw-r--r-- | lib/events/event.cpp | 5 | ||||
-rw-r--r-- | lib/events/event.h | 12 | ||||
-rw-r--r-- | lib/events/eventloader.h | 1 | ||||
-rw-r--r-- | lib/events/simplestateevents.h | 14 | ||||
-rw-r--r-- | lib/events/stateevent.cpp | 15 | ||||
-rw-r--r-- | lib/events/stateevent.h | 20 | ||||
-rw-r--r-- | lib/jobs/basejob.cpp | 19 | ||||
-rw-r--r-- | lib/jobs/basejob.h | 14 | ||||
-rw-r--r-- | lib/jobs/mediathumbnailjob.cpp | 8 | ||||
-rw-r--r-- | lib/jobs/syncjob.cpp | 112 | ||||
-rw-r--r-- | lib/jobs/syncjob.h | 48 | ||||
-rw-r--r-- | lib/logging.h | 11 | ||||
-rw-r--r-- | lib/room.cpp | 498 | ||||
-rw-r--r-- | lib/room.h | 67 | ||||
-rw-r--r-- | lib/syncdata.cpp | 180 | ||||
-rw-r--r-- | lib/syncdata.h | 86 | ||||
-rw-r--r-- | lib/user.cpp | 15 | ||||
-rw-r--r-- | lib/user.h | 2 | ||||
-rw-r--r-- | lib/util.cpp | 14 | ||||
-rw-r--r-- | lib/util.h | 6 |
24 files changed, 804 insertions, 508 deletions
diff --git a/lib/avatar.cpp b/lib/avatar.cpp index b8e1096d..c0ef3cba 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -190,18 +190,8 @@ bool Avatar::Private::checkUrl(const QUrl& url) const return _imageSource != Banned; } -QString cacheLocation() { - const auto cachePath = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - + "/avatar/"; - QDir dir; - if (!dir.exists(cachePath)) - dir.mkpath(cachePath); - return cachePath; -} - QString Avatar::Private::localFile() const { - static const auto cachePath = cacheLocation(); + static const auto cachePath = cacheLocation("avatars"); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } diff --git a/lib/connection.cpp b/lib/connection.cpp index 3d635a7e..9372acd5 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -39,7 +39,6 @@ #include <QtNetwork/QDnsLookup> #include <QtCore/QFile> #include <QtCore/QDir> -#include <QtCore/QFileInfo> #include <QtCore/QStandardPaths> #include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> @@ -65,10 +64,6 @@ HashT erase_if(HashT& hashMap, Pred pred) return removals; } -#ifndef TRIM_RAW_DATA -#define TRIM_RAW_DATA 65535 -#endif - class Connection::Private { public: @@ -228,8 +223,7 @@ void Connection::doConnectToServer(const QString& user, const QString& password, }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { - emit loginError(loginJob->errorString(), - loginJob->rawData(TRIM_RAW_DATA)); + emit loginError(loginJob->errorString(), loginJob->rawDataSample()); }); } @@ -306,7 +300,7 @@ void Connection::sync(int timeout) connect( job, &SyncJob::retryScheduled, this, [this,job] (int retriesTaken, int nextInMilliseconds) { - emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA), + emit networkError(job->errorString(), job->rawDataSample(), retriesTaken, nextInMilliseconds); }); connect( job, &SyncJob::failure, this, [this, job] { @@ -315,14 +309,14 @@ void Connection::sync(int timeout) { qCWarning(SYNCJOB) << "Sync job failed with ContentAccessError - login expired?"; - emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + emit loginError(job->errorString(), job->rawDataSample()); } else - emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + emit syncError(job->errorString(), job->rawDataSample()); }); } -void Connection::onSyncSuccess(SyncData &&data) { +void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) { @@ -343,7 +337,7 @@ void Connection::onSyncSuccess(SyncData &&data) { } if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) { - r->updateData(std::move(roomData)); + r->updateData(std::move(roomData), fromCache); if (d->firstTimeRooms.removeOne(r)) emit loadedRoomState(r); } @@ -422,9 +416,10 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const return callApi<PostReceiptJob>(room->id(), "m.read", event->id()); } -JoinRoomJob* Connection::joinRoom(const QString& roomAlias) +JoinRoomJob* Connection::joinRoom(const QString& roomAlias, + const QStringList& serverNames) { - auto job = callApi<JoinRoomJob>(roomAlias); + auto job = callApi<JoinRoomJob>(roomAlias, serverNames); connect(job, &JoinRoomJob::success, this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); return job; @@ -1063,47 +1058,54 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 8; -static constexpr int CACHE_VERSION_MINOR = 0; +void Connection::saveRoomState(Room* r) const +{ + Q_ASSERT(r); + if (!d->cacheState) + return; + + QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) }; + if (outRoomFile.open(QFile::WriteOnly)) + { + QJsonDocument json { r->toJson() }; + auto data = d->cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); + outRoomFile.write(data.data(), data.size()); + qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); + } else { + qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() + << ":" << outRoomFile.errorString(); + } +} -void Connection::saveState(const QUrl &toFile) const +void Connection::saveState() const { if (!d->cacheState) return; QElapsedTimer et; et.start(); - QFileInfo stateFile { - toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() - }; - if (!stateFile.dir().exists()) - stateFile.dir().mkpath("."); - - QFile outfile { stateFile.absoluteFilePath() }; - if (!outfile.open(QFile::WriteOnly)) + QFile outFile { stateCachePath() % "state.json" }; + if (!outFile.open(QFile::WriteOnly)) { - qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() - << ":" << outfile.errorString(); + qCWarning(MAIN) << "Error opening" << outFile.fileName() + << ":" << outFile.errorString(); qCWarning(MAIN) << "Caching the rooms state disabled"; d->cacheState = false; return; } - QJsonObject rootObj; + QJsonObject rootObj { + { QStringLiteral("cache_version"), QJsonObject { + { QStringLiteral("major"), SyncData::cacheVersion().first }, + { QStringLiteral("minor"), SyncData::cacheVersion().second } + }}}; { QJsonObject rooms; QJsonObject inviteRooms; for (const auto* i : roomMap()) // Pass on rooms in Leave state - { - if (i->joinState() == JoinState::Invite) - inviteRooms.insert(i->id(), i->toJson()); - else - rooms.insert(i->id(), i->toJson()); - QElapsedTimer et1; et1.start(); - QCoreApplication::processEvents(); - if (et1.elapsed() > 1) - qCDebug(PROFILER) << "processEvents() borrowed" << et1; - } + (i->joinState() == JoinState::Invite ? inviteRooms : rooms) + .insert(i->id(), QJsonValue::Null); QJsonObject roomObj; if (!rooms.isEmpty()) @@ -1126,63 +1128,35 @@ void Connection::saveState(const QUrl &toFile) const QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - QJsonObject versionObj; - versionObj.insert("major", CACHE_VERSION_MAJOR); - versionObj.insert("minor", CACHE_VERSION_MINOR); - rootObj.insert("cache_version", versionObj); - QJsonDocument json { rootObj }; auto data = d->cacheToBinary ? json.toBinaryData() : json.toJson(QJsonDocument::Compact); qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; - outfile.write(data.data(), data.size()); - qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); + outFile.write(data.data(), data.size()); + qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); } -void Connection::loadState(const QUrl &fromFile) +void Connection::loadState() { if (!d->cacheState) return; QElapsedTimer et; et.start(); - QFile file { - fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() - }; - if (!file.exists()) - { - qCDebug(MAIN) << "No state cache file found"; - return; - } - if(!file.open(QFile::ReadOnly)) - { - qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; - return; - } - QByteArray data = file.readAll(); - auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : - QJsonDocument::fromJson(data); - if (jsonDoc.isNull()) - { - qCWarning(MAIN) << "Cache file broken, discarding"; + SyncData sync { stateCachePath() % "state.json" }; + if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; - } - auto actualCacheVersionMajor = - jsonDoc.object() - .value("cache_version").toObject() - .value("major").toInt(); - if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) + + if (!sync.unresolvedRooms().isEmpty()) { - qCWarning(MAIN) - << "Major version of the cache file is" << actualCacheVersionMajor - << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; + qCWarning(MAIN) << "State cache incomplete, discarding"; return; } - - SyncData sync; - sync.parseJson(jsonDoc); - onSyncSuccess(std::move(sync)); + // TODO: to handle load failures, instead of the above block: + // 1. Do initial sync on failed rooms without saving the nextBatch token + // 2. Do the sync across all rooms as normal + onSyncSuccess(std::move(sync), true); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } @@ -1190,8 +1164,7 @@ QString Connection::stateCachePath() const { auto safeUserId = userId(); safeUserId.replace(':', '_'); - return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - % '/' % safeUserId % "_state.json"; + return cacheLocation(safeUserId); } bool Connection::cacheState() const diff --git a/lib/connection.h b/lib/connection.h index cfad8774..32533b6e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -280,7 +280,7 @@ namespace QMatrixClient * to be QML-friendly. Empty parameter means using a path * defined by stateCachePath(). */ - Q_INVOKABLE void loadState(const QUrl &fromFile = {}); + Q_INVOKABLE void loadState(); /** * This method saves the current state of rooms (but not messages * in them) to a local cache file, so that it could be loaded by @@ -290,7 +290,10 @@ namespace QMatrixClient * QML-friendly. Empty parameter means using a path defined by * stateCachePath(). */ - Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; + Q_INVOKABLE void saveState() const; + + /// This method saves the current state of a single room. + void saveRoomState(Room* r) const; /** * The default path to store the cached room state, defined as @@ -461,7 +464,8 @@ namespace QMatrixClient CreateRoomJob* createDirectChat(const QString& userId, const QString& topic = {}, const QString& name = {}); - virtual JoinRoomJob* joinRoom(const QString& roomAlias); + virtual JoinRoomJob* joinRoom(const QString& roomAlias, + const QStringList& serverNames = {}); /** Sends /forget to the server and also deletes room locally. * This method is in Connection, not in Room, since it's a @@ -518,7 +522,7 @@ namespace QMatrixClient * a successful login and logout and are constant at other times. */ void stateChanged(); - void loginError(QString message, QByteArray details); + void loginError(QString message, QString details); /** A network request (job) failed * @@ -536,11 +540,11 @@ namespace QMatrixClient * @param retriesTaken - how many retries have already been taken * @param nextRetryInMilliseconds - when the job will retry again */ - void networkError(QString message, QByteArray details, + void networkError(QString message, QString details, int retriesTaken, int nextRetryInMilliseconds); void syncDone(); - void syncError(QString message, QByteArray details); + void syncError(QString message, QString details); void newUser(User* user); @@ -673,7 +677,7 @@ namespace QMatrixClient /** * Completes loading sync data. */ - void onSyncSuccess(SyncData &&data); + void onSyncSuccess(SyncData &&data, bool fromCache = false); private: class Private; diff --git a/lib/converters.h b/lib/converters.h index 70938ab9..53855a1f 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -61,7 +61,7 @@ namespace QMatrixClient inline auto toJson(const QJsonValue& val) { return val; } inline auto toJson(const QJsonObject& o) { return o; } inline auto toJson(const QJsonArray& arr) { return arr; } - // Special-case QStrings and bools to avoid ambiguity between QJsonValue + // Special-case QString to avoid ambiguity between QJsonValue // and QVariant (also, QString.isEmpty() is used in _impl::AddNode<> below) inline auto toJson(const QString& s) { return s; } diff --git a/lib/events/event.cpp b/lib/events/event.cpp index fd6e3939..c98dfbb6 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -77,3 +77,8 @@ const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); } + +void Event::dumpTo(QDebug dbg) const +{ + dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); +} diff --git a/lib/events/event.h b/lib/events/event.h index 5b33628f..c51afcc4 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -209,7 +209,7 @@ namespace QMatrixClient inline auto registerEventType() { static const auto _ = setupFactory<EventT>(); - return _; + return _; // Only to facilitate usage in static initialisation } // === Event === @@ -257,8 +257,18 @@ namespace QMatrixClient return fromJson<T>(contentJson()[key]); } + friend QDebug operator<<(QDebug dbg, const Event& e) + { + QDebugStateSaver _dss { dbg }; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.type() << "): "; + e.dumpTo(dbg); + return dbg; + } + virtual bool isStateEvent() const { return false; } virtual bool isCallEvent() const { return false; } + virtual void dumpTo(QDebug dbg) const; protected: QJsonObject& editJson() { return _json; } diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 3ee9a181..cd2f9149 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "converters.h" namespace QMatrixClient { namespace _impl { diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 56be947c..5aa24c15 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -59,21 +59,23 @@ namespace QMatrixClient }; } // namespace EventContent -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \ { \ public: \ - using content_type = _ContentType; \ + using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \ - { } \ + explicit _Name() : _Name(value_type()) { } \ template <typename T> \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), \ QStringLiteral(#_ContentKey), \ std::forward<T>(value)) \ { } \ + explicit _Name(QJsonObject obj) \ + : StateEvent(typeId(), std::move(obj), \ + QStringLiteral(#_ContentKey)) \ + { } \ auto _ContentKey() const { return content().value; } \ }; \ REGISTER_EVENT_TYPE(_Name) \ diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index fd5d2642..fd8079be 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -28,3 +28,18 @@ bool StateEventBase::repeatsState() const const auto prevContentJson = unsignedJson().value(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } + +QString StateEventBase::replacedState() const +{ + return unsignedJson().value("replaces_state"_ls).toString(); +} + +void StateEventBase::dumpTo(QDebug dbg) const +{ + if (!stateKey().isEmpty()) + dbg << '<' << stateKey() << "> "; + if (unsignedJson().contains(PrevContentKeyL)) + dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) + .toJson(QJsonDocument::Compact) << " -> "; + RoomEvent::dumpTo(dbg); +} diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 6032132e..d4a7e8b3 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -30,11 +30,21 @@ namespace QMatrixClient { ~StateEventBase() override = default; bool isStateEvent() const override { return true; } + QString replacedState() const; + void dumpTo(QDebug dbg) const override; + virtual bool repeatsState() const; }; using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; + /** + * A combination of event type and state key uniquely identifies a piece + * of state in Matrix. + * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events + */ + using StateEventKey = std::pair<QString, QString>; + template <typename ContentT> struct Prev { @@ -90,3 +100,13 @@ namespace QMatrixClient { std::unique_ptr<Prev<ContentT>> _prev; }; } // namespace QMatrixClient + +namespace std { + template <> struct hash<QMatrixClient::StateEventKey> + { + size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT + { + return qHash(k); + } + }; +} diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b21173ae..4a7780b1 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -426,8 +426,8 @@ BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); if( error.error == QJsonParseError::NoError ) return parseJson(json); - else - return { IncorrectResponseError, error.errorString() }; + + return { IncorrectResponseError, error.errorString() }; } BaseJob::Status BaseJob::parseJson(const QJsonDocument&) @@ -519,8 +519,19 @@ BaseJob::Status BaseJob::status() const QByteArray BaseJob::rawData(int bytesAtMost) const { - return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ? - d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse; + return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost + ? d->rawResponse.left(bytesAtMost) : d->rawResponse; +} + +QString BaseJob::rawDataSample(int bytesAtMost) const +{ + auto data = rawData(bytesAtMost); + Q_ASSERT(data.size() <= d->rawResponse.size()); + return data.size() == d->rawResponse.size() + ? data : data + tr("...(truncated, %Ln bytes in total)", + "Comes after trimmed raw network response", + d->rawResponse.size()); + } QString BaseJob::statusCaption() const diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4ef25ab8..3d50344d 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -138,8 +138,20 @@ namespace QMatrixClient Status status() const; /** Short human-friendly message on the job status */ QString statusCaption() const; - /** Raw response body as received from the server */ + /** Get raw response body as received from the server + * \param bytesAtMost return this number of leftmost bytes, or -1 + * to return the entire response + */ QByteArray rawData(int bytesAtMost = -1) const; + /** Get UI-friendly sample of raw data + * + * This is almost the same as rawData but appends the "truncated" + * suffix if not all data fit in bytesAtMost. This call is + * recommended to present a sample of raw data as "details" next to + * error messages. Note that the default \p bytesAtMost value is + * also tailored to UI cases. + */ + QString rawDataSample(int bytesAtMost = 65535) const; /** Error (more generally, status) code * Equivalent to status().code diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index 8dfb6094..aeb49839 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -23,7 +23,8 @@ using namespace QMatrixClient; QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, QSize requestedSize) { - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1), + return makeRequestUrl(std::move(baseUrl), + mxcUri.authority(), mxcUri.path().mid(1), requestedSize.width(), requestedSize.height()); } @@ -34,9 +35,8 @@ MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, { } MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) - : GetContentThumbnailJob(mxcUri.authority(), - mxcUri.path().mid(1), // sans leading '/' - requestedSize.width(), requestedSize.height()) + : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/' + requestedSize) { } QImage MediaThumbnailJob::thumbnail() const diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 9cbac71b..ac0f6685 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -18,10 +18,6 @@ #include "syncjob.h" -#include "events/eventloader.h" - -#include <QtCore/QElapsedTimer> - using namespace QMatrixClient; static size_t jobId = 0; @@ -46,110 +42,14 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, setMaxRetries(std::numeric_limits<int>::max()); } -QString SyncData::nextBatch() const -{ - return nextBatch_; -} - -SyncDataList&& SyncData::takeRoomData() -{ - return std::move(roomData); -} - -Events&& SyncData::takePresenceData() -{ - return std::move(presenceData); -} - -Events&& SyncData::takeAccountData() -{ - return std::move(accountData); -} - -Events&&SyncData::takeToDeviceEvents() -{ - return std::move(toDeviceEvents); -} - -template <typename EventsArrayT, typename StrT> -inline EventsArrayT load(const QJsonObject& batches, StrT keyName) -{ - return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); -} - BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { - return d.parseJson(data); -} - -BaseJob::Status SyncData::parseJson(const QJsonDocument &data) -{ - QElapsedTimer et; et.start(); - - auto json = data.object(); - nextBatch_ = json.value("next_batch"_ls).toString(); - presenceData = load<Events>(json, "presence"_ls); - accountData = load<Events>(json, "account_data"_ls); - toDeviceEvents = load<Events>(json, "to_device"_ls); + d.parseJson(data.object()); + if (d.unresolvedRooms().isEmpty()) + return BaseJob::Success; - auto rooms = json.value("rooms"_ls).toObject(); - JoinStates::Int ii = 1; // ii is used to make a JoinState value - auto totalRooms = 0; - auto totalEvents = 0; - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) - { - const auto rs = rooms.value(JoinStateStrings[i]).toObject(); - // We have a Qt container on the right and an STL one on the left - roomData.reserve(static_cast<size_t>(rs.size())); - for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) - { - roomData.emplace_back(roomIt.key(), JoinState(ii), - roomIt.value().toObject()); - const auto& r = roomData.back(); - totalEvents += r.state.size() + r.ephemeral.size() + - r.accountData.size() + r.timeline.size(); - } - totalRooms += roomData.size(); - } - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; - return BaseJob::Success; + qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + << d.unresolvedRooms().join(','); + return BaseJob::IncorrectResponseError; } -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-qmatrixclient.unread_count"); - -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , state(load<StateEvents>(room_, - joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) -{ - switch (joinState) { - case JoinState::Join: - ephemeral = load<Events>(room_, "ephemeral"_ls); - FALLTHROUGH; - case JoinState::Leave: - { - accountData = load<Events>(room_, "account_data"_ls); - timeline = load<RoomEvents>(room_, "timeline"_ls); - const auto timelineJson = room_.value("timeline"_ls).toObject(); - timelineLimited = timelineJson.value("limited"_ls).toBool(); - timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); - - break; - } - default: /* nothing on top of state */; - } - - const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count"_ls).toInt(); - notificationCount = unreadJson.value("notification_count"_ls).toInt(); - if (highlightCount > 0 || notificationCount > 0) - qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << highlightCount - << "and notifications:" << notificationCount; -} diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 6b9bedfa..a0a3c026 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -20,56 +20,10 @@ #include "basejob.h" -#include "joinstate.h" -#include "events/stateevent.h" -#include "util.h" +#include "../syncdata.h" namespace QMatrixClient { - class SyncRoomData - { - public: - QString roomId; - JoinState joinState; - StateEvents state; - RoomEvents timeline; - Events ephemeral; - Events accountData; - - bool timelineLimited; - QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; - - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); - SyncRoomData(SyncRoomData&&) = default; - SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; - }; - // QVector cannot work with non-copiable objects, std::vector can. - using SyncDataList = std::vector<SyncRoomData>; - - class SyncData - { - public: - BaseJob::Status parseJson(const QJsonDocument &data); - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); - SyncDataList&& takeRoomData(); - QString nextBatch() const; - - private: - QString nextBatch_; - Events presenceData; - Events accountData; - Events toDeviceEvents; - SyncDataList roomData; - }; - class SyncJob: public BaseJob { public: diff --git a/lib/logging.h b/lib/logging.h index 8dbfdf30..a3a65887 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -65,6 +65,17 @@ namespace QMatrixClient { return qdm(debug_object); } + + inline qint64 profilerMinNsecs() + { + return +#ifdef PROFILER_LOG_USECS + PROFILER_LOG_USECS +#else + 200 +#endif + * 1000; + } } inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et) diff --git a/lib/room.cpp b/lib/room.cpp index 7c45bf89..8b81bfb2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -25,7 +25,6 @@ #include "csapi/receipts.h" #include "csapi/redaction.h" #include "csapi/account-data.h" -#include "csapi/message_pagination.h" #include "csapi/room_state.h" #include "csapi/room_send.h" #include "csapi/tags.h" @@ -46,10 +45,10 @@ #include "connection.h" #include "user.h" #include "converters.h" +#include "syncdata.h" #include <QtCore/QHash> #include <QtCore/QStringBuilder> // for efficient string concats (operator%) -#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> #include <QtCore/QDir> #include <QtCore/QTemporaryFile> @@ -92,18 +91,19 @@ class Room::Private void updateDisplayname(); Connection* connection; + QString id; + JoinState joinState; + /// The state of the room at timeline position before-0 + /// \sa timelineBase + std::unordered_map<StateEventKey, StateEventPtr> baseState; + /// The state of the room at timeline position after-maxTimelineIndex() + /// \sa Room::syncEdge + QHash<StateEventKey, const StateEventBase*> currentState; Timeline timeline; PendingEvents unsyncedEvents; QHash<QString, TimelineItem::index_t> eventsIndex; - QString id; - QStringList aliases; - QString canonicalAlias; - QString name; QString displayname; - QString topic; - QString encryptionAlgorithm; Avatar avatar; - JoinState joinState; int highlightCount = 0; int notificationCount = 0; members_map_t membersMap; @@ -157,8 +157,8 @@ class Room::Private fileTransfers[tid].status = FileTransferInfo::Failed; emit q->fileTransferFailed(tid, errorMessage); } - // A map from event/txn ids to information about the long operation; - // used for both download and upload operations + /// A map from event/txn ids to information about the long operation; + /// used for both download and upload operations QHash<QString, FileTransferPrivateInfo> fileTransfers; const RoomMessageEvent* getEventWithFile(const QString& eventId) const; @@ -169,8 +169,22 @@ class Room::Private void renameMember(User* u, QString oldName); void removeMemberFromMap(const QString& username, User* u); + /// A point in the timeline corresponding to baseState + rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + void getPreviousContent(int limit = 10); + template <typename EventT> + const EventT* getCurrentState(QString stateKey = {}) const + { + static const EventT empty; + const auto* evt = + currentState.value({EventT::matrixTypeId(), stateKey}, &empty); + Q_ASSERT(evt->type() == EventT::typeId() && + evt->matrixType() == EventT::matrixTypeId()); + return static_cast<const EventT*>(evt); + } + bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && @@ -178,7 +192,7 @@ class Room::Private is<RoomMessageEvent>(*ti); } - void addNewMessageEvents(RoomEvents&& events); + Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline @@ -215,6 +229,8 @@ class Room::Private QString doSendEvent(const RoomEvent* pEvent); PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); + void onEventSendingFailure(const RoomEvent* pEvent, + const QString& txnId, BaseJob* call = nullptr); template <typename EvT> auto requestSetState(const QString& stateKey, const EvT& event) @@ -288,17 +304,17 @@ const Room::PendingEvents& Room::pendingEvents() const QString Room::name() const { - return d->name; + return d->getCurrentState<RoomNameEvent>()->name(); } QStringList Room::aliases() const { - return d->aliases; + return d->getCurrentState<RoomAliasesEvent>()->aliases(); } QString Room::canonicalAlias() const { - return d->canonicalAlias; + return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); } QString Room::displayName() const @@ -308,7 +324,7 @@ QString Room::displayName() const QString Room::topic() const { - return d->topic; + return d->getCurrentState<RoomTopicEvent>()->topic(); } QString Room::avatarMediaId() const @@ -366,6 +382,7 @@ void Room::setJoinState(JoinState state) d->joinState = state; qCDebug(MAIN) << "Room" << id() << "changed state: " << int(oldState) << "->" << int(state); + emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } @@ -384,6 +401,7 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi<PostReadMarkersJob>(id, storedId); emit q->readMarkerMoved(eventId, storedId); + connection->saveRoomState(q); } } @@ -408,7 +426,7 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) QElapsedTimer et; et.start(); const auto newUnreadMessages = count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.nsecsElapsed() > 10000) + if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages took" << et; if(newUnreadMessages > 0) @@ -418,7 +436,7 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) unreadMessages = 0; unreadMessages += newUnreadMessages; - qCDebug(MAIN) << "Room" << displayname << "has gained" + qCDebug(MAIN) << "Room" << q->objectName() << "has gained" << newUnreadMessages << "unread message(s)," << (q->readMarker() == timeline.crend() ? "in total at least" : "in total") @@ -450,7 +468,7 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) QElapsedTimer et; et.start(); unreadMessages = count_if(eagerMarker, timeline.cend(), std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.nsecsElapsed() > 10000) + if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count @@ -513,11 +531,21 @@ int Room::unreadCount() const return d->unreadMessages; } -Room::rev_iter_t Room::timelineEdge() const +Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } +Room::Timeline::const_iterator Room::syncEdge() const +{ + return d->timeline.cend(); +} + +Room::rev_iter_t Room::timelineEdge() const +{ + return historyEdge(); +} + TimelineItem::index_t Room::minTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.front().index(); @@ -945,7 +973,12 @@ int Room::timelineSize() const bool Room::usesEncryption() const { - return !d->encryptionAlgorithm.isEmpty(); + return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); +} + +GetRoomEventsJob* Room::eventsHistoryJob() const +{ + return d->eventsHistoryJob; } void Room::Private::insertMemberIntoMap(User *u) @@ -1006,8 +1039,9 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline( { Q_ASSERT(!events.empty()); // Historical messages arrive in newest-to-oldest order, so the process for - // them is symmetric to the one for new messages. - auto index = timeline.empty() ? -int(placement) : + // them is almost symmetric to the one for new messages. New messages get + // appended from index 0; old messages go backwards from index -1. + auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ : placement == Older ? timeline.front().index() : timeline.back().index(); auto baseIndex = index; @@ -1028,7 +1062,7 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline( eventsIndex.insert(eId, index); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } - const auto insertedSize = (index - baseIndex) * int(placement); + const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); return insertedSize; } @@ -1074,45 +1108,48 @@ QString Room::roomMembername(const QString& userId) const return roomMembername(user(userId)); } -void Room::updateData(SyncRoomData&& data) +void Room::updateData(SyncRoomData&& data, bool fromCache) { if( d->prevBatch.isEmpty() ) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); + Changes roomChanges = Change::NoChange; QElapsedTimer et; et.start(); for (auto&& event: data.accountData) - processAccountDataEvent(move(event)); + roomChanges |= processAccountDataEvent(move(event)); - bool emitNamesChanged = false; if (!data.state.empty()) { et.restart(); - for (const auto& e: data.state) - emitNamesChanged |= processStateEvent(*e); + for (auto&& eptr: data.state) + { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); + roomChanges |= processStateEvent(evt); + } - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; + if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::processStateEvents():" + << data.state.size() << "event(s)," << et; } if (!data.timeline.empty()) { et.restart(); - // State changes can arrive in a timeline event; so check those. - for (const auto& e: data.timeline) - emitNamesChanged |= processStateEvent(*e); - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" - << data.timeline.size() << "event(s)," << et; + roomChanges |= d->addNewMessageEvents(move(data.timeline)); + if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" + << data.timeline.size() << "event(s)," << et; } - if (emitNamesChanged) + if (roomChanges&TopicChange) + emit topicChanged(); + + if (roomChanges&NameChange) emit namesChanged(this); + d->updateDisplayname(); - if (!data.timeline.empty()) - { - et.restart(); - d->addNewMessageEvents(move(data.timeline)); - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } for( auto&& ephemeralEvent: data.ephemeral ) processEphemeralEvent(move(ephemeralEvent)); @@ -1134,6 +1171,12 @@ void Room::updateData(SyncRoomData&& data) d->notificationCount = data.notificationCount; emit notificationCountChanged(this); } + if (roomChanges != Change::NoChange) + { + emit changed(roomChanges); + if (!fromCache) + connection()->saveRoomState(this); + } } QString Room::Private::sendEvent(RoomEventPtr&& event) @@ -1150,50 +1193,42 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) QString Room::Private::doSendEvent(const RoomEvent* pEvent) { auto txnId = pEvent->transactionId(); - // TODO: Enqueue the job rather than immediately trigger it - auto call = connection->callApi<SendMessageJob>(BackgroundRequest, - id, pEvent->matrixType(), txnId, pEvent->contentJson()); - Room::connect(call, &BaseJob::started, q, - [this,pEvent,txnId] { - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { - qWarning(EVENTS) << "Pending event for transaction" << txnId - << "not found - got synced so soon?"; - return; - } - it->setDeparted(); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - }); - Room::connect(call, &BaseJob::failure, q, - [this,pEvent,txnId,call] { - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { - qCritical(EVENTS) << "Pending event for transaction" << txnId - << "got lost without successful sending"; - return; - } - it->setSendingFailed( - call->statusCaption() % ": " % call->errorString()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - - }); - Room::connect(call, &BaseJob::success, q, - [this,call,pEvent,txnId] { - // Find an event by the pointer saved in the lambda (the pointer - // may be dangling by now but we can still search by it). - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { - qDebug(EVENTS) << "Pending event for transaction" << txnId - << "already merged"; - return; - } + // TODO, #133: Enqueue the job rather than immediately trigger it. + if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, + id, pEvent->matrixType(), txnId, pEvent->contentJson())) + { + Room::connect(call, &BaseJob::started, q, + [this,pEvent,txnId] { + auto it = findAsPending(pEvent); + if (it == unsyncedEvents.end()) + { + qWarning(EVENTS) << "Pending event for transaction" << txnId + << "not found - got synced so soon?"; + return; + } + it->setDeparted(); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); + }); + Room::connect(call, &BaseJob::failure, q, + std::bind(&Room::Private::onEventSendingFailure, + this, pEvent, txnId, call)); + Room::connect(call, &BaseJob::success, q, + [this,call,pEvent,txnId] { + // Find an event by the pointer saved in the lambda (the pointer + // may be dangling by now but we can still search by it). + auto it = findAsPending(pEvent); + if (it == unsyncedEvents.end()) + { + qDebug(EVENTS) << "Pending event for transaction" << txnId + << "already merged"; + return; + } - it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - }); + it->setReachedServer(call->eventId()); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); + }); + } else + onEventSendingFailure(pEvent, txnId); return txnId; } @@ -1206,6 +1241,22 @@ Room::PendingEvents::iterator Room::Private::findAsPending( return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp); } +void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, + const QString& txnId, BaseJob* call) +{ + auto it = findAsPending(pEvent); + if (it == unsyncedEvents.end()) + { + qCritical(EVENTS) << "Pending event for transaction" << txnId + << "could not be sent"; + return; + } + it->setSendingFailed(call + ? call->statusCaption() % ": " % call->errorString() + : tr("The call could not be started")); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); +} + QString Room::retryMessage(const QString& txnId) { auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), @@ -1285,15 +1336,15 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) if (le->type() != re->type()) return false; - if (!le->id().isEmpty()) + if (!re->id().isEmpty()) return le->id() == re->id(); - if (!le->transactionId().isEmpty()) + if (!re->transactionId().isEmpty()) return le->transactionId() == re->transactionId(); // This one is not reliable (there can be two unsynced // events with the same type, sender and state key) but // it's the best we have for state events. - if (le->isStateEvent()) + if (re->isStateEvent()) return le->stateKey() == re->stateKey(); // Empty id and no state key, hmm... (shrug) @@ -1349,10 +1400,13 @@ void Room::Private::getPreviousContent(int limit) { eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); + emit q->eventsHistoryJobChanged(); connect( eventsHistoryJob, &BaseJob::success, q, [=] { prevBatch = eventsHistoryJob->end(); addHistoricalMessageEvents(eventsHistoryJob->chunk()); }); + connect( eventsHistoryJob, &QObject::destroyed, + q, &Room::eventsHistoryJobChanged); } } @@ -1587,11 +1641,26 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) return true; } - // Make a new event from the redacted JSON, exchange events, - // notify everyone and delete the old event + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); - q->onRedaction(*oldEvent, *ti.event()); qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + if (oldEvent->isStateEvent()) + { + const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; + Q_ASSERT(currentState.contains(evtKey)); + if (currentState[evtKey] == oldEvent.get()) + { + Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState + qCDebug(MAIN).nospace() << "Reverting state " + << oldEvent->matrixType() << "/" << oldEvent->stateKey(); + // Retarget the current state to the newly made event. + if (q->processStateEvent(*ti)) + emit q->namesChanged(q); + updateDisplayname(); + } + } + q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -1613,48 +1682,49 @@ inline bool isRedaction(const RoomEventPtr& ep) return is<RedactionEvent>(*ep); } -void Room::Private::addNewMessageEvents(RoomEvents&& events) +Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); + if (events.empty()) + return Change::NoChange; // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. - // XXX: The code below is written (and commented) so that it could be - // quickly converted to not-saving redaction events in the timeline. - // See #220 for details. - auto newEnd = std::find_if(events.begin(), events.end(), isRedaction); - // Either process the redaction, or shift the non-redaction event - // overwriting redactions in a remove_if fashion. - for(const auto& eptr: RoomEventsRange(newEnd, events.end())) + // NB: We have to store redaction events to the timeline too - see #220. + auto redactionIt = std::find_if(events.begin(), events.end(), isRedaction); + for(const auto& eptr: RoomEventsRange(redactionIt, events.end())) if (auto* r = eventCast<RedactionEvent>(eptr)) { // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; - auto targetIt = std::find_if(events.begin(), newEnd, + auto targetIt = std::find_if(events.begin(), redactionIt, [id=r->redactedEvent()] (const RoomEventPtr& ep) { return ep->id() == id; }); - if (targetIt != newEnd) + if (targetIt != redactionIt) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(MAIN) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; - // If the target events comes later, it comes already redacted. + // If the target event comes later, it comes already redacted. } -// else // This should be uncommented once we stop adding redactions to the timeline -// *newEnd++ = std::move(eptr); - newEnd = events.end(); // This line should go if/when we stop adding redactions to the timeline - if (events.begin() == newEnd) - return; + // State changes arrive as a part of timeline; the current room state gets + // updated before merging events to the timeline because that's what + // clients historically expect. This may eventually change though if we + // postulate that the current state is only current between syncs but not + // within a sync. + Changes stateChanges = Change::NoChange; + for (const auto& eptr: events) + stateChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); auto totalInserted = 0; - for (auto it = events.begin(); it != newEnd;) + for (auto it = events.begin(); it != events.end();) { - auto nextPendingPair = findFirstOf(it, newEnd, + auto nextPendingPair = findFirstOf(it, events.end(), unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent); auto nextPending = nextPendingPair.first; @@ -1668,7 +1738,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) q->onAddNewTimelineEvents(firstInserted); emit q->addedMessages(firstInserted->index(), timeline.back().index()); } - if (nextPending == newEnd) + if (nextPending == events.end()) break; it = nextPending + 1; @@ -1696,7 +1766,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) if (totalInserted > 0) { qCDebug(MAIN) - << "Room" << displayname << "received" << totalInserted + << "Room" << q->objectName() << "received" << totalInserted << "new events; the last event is now" << timeline.back(); // The first event in the just-added batch (referred to by `from`) @@ -1717,21 +1787,34 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + return stateChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { + QElapsedTimer et; et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); - RoomEventsRange normalEvents { - events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction) - }; - if (normalEvents.empty()) + if (events.empty()) return; - emit q->aboutToAddHistoricalMessages(normalEvents); - const auto insertedSize = moveEventsToTimeline(normalEvents, Older); + // In case of lazy-loading new members may be loaded with historical + // messages. Also, the cache doesn't store events with empty content; + // so when such events show up in the timeline they should be properly + // incorporated. + for (const auto& eptr: events) + { + const auto& e = *eptr; + if (e.isStateEvent() && + !currentState.contains({e.matrixType(), e.stateKey()})) + { + q->processStateEvent(e); + } + } + + emit q->aboutToAddHistoricalMessages(events); + const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize @@ -1743,51 +1826,46 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) updateUnreadCount(from, timeline.crend()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); + if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():" + << insertedSize << "event(s)," << et; } -bool Room::processStateEvent(const RoomEvent& e) +Room::Changes Room::processStateEvent(const RoomEvent& e) { + if (!e.isStateEvent()) + return Change::NoChange; + + d->currentState[{e.matrixType(),e.stateKey()}] = + static_cast<const StateEventBase*>(&e); + if (!is<RoomMemberEvent>(e)) + qCDebug(EVENTS) << "Room state event:" << e; + return visit(e - , [this] (const RoomNameEvent& evt) { - d->name = evt.name(); - qCDebug(MAIN) << "Room name updated:" << d->name; - return true; + , [] (const RoomNameEvent&) { + return NameChange; } - , [this] (const RoomAliasesEvent& evt) { - d->aliases = evt.aliases(); - qCDebug(MAIN) << "Room aliases updated:" << d->aliases; - return true; + , [] (const RoomAliasesEvent&) { + return OtherChange; } , [this] (const RoomCanonicalAliasEvent& evt) { - d->canonicalAlias = evt.alias(); - if (!d->canonicalAlias.isEmpty()) - setObjectName(d->canonicalAlias); - qCDebug(MAIN) << "Room canonical alias updated:" - << d->canonicalAlias; - return true; + setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); + return CanonicalAliasChange; } - , [this] (const RoomTopicEvent& evt) { - d->topic = evt.topic(); - qCDebug(MAIN) << "Room topic updated:" << d->topic; - emit topicChanged(); - return false; + , [] (const RoomTopicEvent&) { + return TopicChange; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) - { - qCDebug(MAIN) << "Room avatar URL updated:" - << evt.url().toString(); emit avatarChanged(); - } - return false; + return AvatarChange; } , [this] (const RoomMemberEvent& evt) { auto* u = user(evt.userId()); u->processEvent(evt, this); if (u == localUser() && memberJoinState(u) == JoinState::Invite && evt.isDirect()) - connection()->addToDirectChats(this, - user(evt.senderId())); + connection()->addToDirectChats(this, user(evt.senderId())); if( evt.membership() == MembershipType::Join ) { @@ -1807,24 +1885,23 @@ bool Room::processStateEvent(const RoomEvent& e) emit userAdded(u); } } - else if( evt.membership() == MembershipType::Leave ) + else if( evt.membership() != MembershipType::Join ) { if (memberJoinState(u) == JoinState::Join) { + if (evt.membership() == MembershipType::Invite) + qCWarning(MAIN) << "Invalid membership change:" << evt; if (!d->membersLeft.contains(u)) d->membersLeft.append(u); d->removeMemberFromMap(u->name(this), u); emit userRemoved(u); } } - return false; + return MembersChange; } - , [this] (const EncryptionEvent& evt) { - d->encryptionAlgorithm = evt.algorithm(); - qCDebug(MAIN) << "Encryption switched on in room" << id() - << "with algorithm" << d->encryptionAlgorithm; - emit encryption(); - return false; + , [this] (const EncryptionEvent&) { + emit encryption(); // It can only be done once, so emit it here. + return EncryptionOn; } ); } @@ -1841,15 +1918,17 @@ void Room::processEphemeralEvent(EventPtr&& event) if (memberJoinState(u) == JoinState::Join) d->usersTyping.append(u); } - if (!evt->users().isEmpty()) + if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" << evt->users().size() << "users," << et; emit typingChanged(); } if (auto* evt = eventCast<ReceiptEvent>(event)) { + int totalReceipts = 0; for( const auto &p: qAsConst(evt->eventsWithReceipts()) ) { + totalReceipts += p.receipts.size(); { if (p.receipts.size() == 1) qCDebug(EPHEMERAL) << "Marking" << p.evtId @@ -1888,14 +1967,15 @@ void Room::processEphemeralEvent(EventPtr&& event) } } } - if (!evt->eventsWithReceipts().isEmpty()) + if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 || + et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" << evt->eventsWithReceipts().size() - << "events with receipts," << et; + << "event(s) with" << totalReceipts << "receipt(s)," << et; } } -void Room::processAccountDataEvent(EventPtr&& event) +Room::Changes Room::processAccountDataEvent(EventPtr&& event) { if (auto* evt = eventCast<TagEvent>(event)) d->setTags(evt->tags()); @@ -1922,7 +2002,9 @@ void Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); + return Change::AccountDataChange; } + return Change::NoChange; } QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const @@ -1963,9 +2045,8 @@ QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) co // iii. More users. if (userlist.size() > 3) - return tr("%1 and %L2 others") - .arg(q->roomMembername(first_two[0])) - .arg(userlist.size() - 3); + return tr("%1 and %Ln other(s)", "", userlist.size() - 3) + .arg(q->roomMembername(first_two[0])); // userlist.size() < 2 - apparently, there's only current user in the room return QString(); @@ -1977,27 +2058,29 @@ QString Room::Private::calculateDisplayname() const // Numbers below refer to respective parts in the spec. // 1. Name (from m.room.name) - if (!name.isEmpty()) { - return name; + auto dispName = q->name(); + if (!dispName.isEmpty()) { + return dispName; } // 2. Canonical alias - if (!canonicalAlias.isEmpty()) - return canonicalAlias; + dispName = q->canonicalAlias(); + if (!dispName.isEmpty()) + return dispName; // Using m.room.aliases in naming is explicitly discouraged by the spec - //if (!aliases.empty() && !aliases.at(0).isEmpty()) - // return aliases.at(0); + //if (!q->aliases().empty() && !q->aliases().at(0).isEmpty()) + // return q->aliases().at(0); // 3. Room members - QString topMemberNames = roomNameFromMemberNames(membersMap.values()); - if (!topMemberNames.isEmpty()) - return topMemberNames; + dispName = roomNameFromMemberNames(membersMap.values()); + if (!dispName.isEmpty()) + return dispName; // 4. Users that previously left the room - topMemberNames = roomNameFromMemberNames(membersLeft); - if (!topMemberNames.isEmpty()) - return tr("Empty room (was: %1)").arg(topMemberNames); + dispName = roomNameFromMemberNames(membersLeft); + if (!dispName.isEmpty()) + return tr("Empty room (was: %1)").arg(dispName); // 5. Fail miserably return tr("Empty room (%1)").arg(id); @@ -2016,34 +2099,6 @@ void Room::Private::updateDisplayname() } } -void appendStateEvent(QJsonArray& events, const QString& type, - const QJsonObject& content, const QString& stateKey = {}) -{ - if (!content.isEmpty() || !stateKey.isEmpty()) - { - auto json = basicEventJson(type, content); - json.insert(QStringLiteral("state_key"), stateKey); - events.append(json); - } -} - -#define ADD_STATE_EVENT(events, type, name, content) \ - appendStateEvent((events), QStringLiteral(type), \ - {{ QStringLiteral(name), content }}); - -void appendEvent(QJsonArray& events, const QString& type, - const QJsonObject& content) -{ - if (!content.isEmpty()) - events.append(basicEventJson(type, content)); -} - -template <typename EvtT> -void appendEvent(QJsonArray& events, const EvtT& event) -{ - appendEvent(events, EvtT::matrixTypeId(), event.toJson()); -} - QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); @@ -2051,23 +2106,19 @@ QJsonObject Room::Private::toJson() const { QJsonArray stateEvents; - ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name); - ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic); - ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url", - avatar.url().toString()); - ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases", - QJsonArray::fromStringList(aliases)); - ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", - canonicalAlias); - ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", - encryptionAlgorithm); - - for (const auto *m : membersMap) - appendStateEvent(stateEvents, QStringLiteral("m.room.member"), - { { QStringLiteral("membership"), QStringLiteral("join") } - , { QStringLiteral("displayname"), m->rawName(q) } - , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } - }, m->id()); + for (const auto* evt: currentState) + { + Q_ASSERT(evt->isStateEvent()); + if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) || + evt->contentJson().isEmpty()) + continue; + + auto json = evt->fullJson(); + auto unsignedJson = evt->unsignedJson(); + unsignedJson.remove(QStringLiteral("prev_content")); + json[UnsignedKeyL] = unsignedJson; + stateEvents.append(json); + } const auto stateObjName = joinState == JoinState::Invite ? QStringLiteral("invite_state") : QStringLiteral("state"); @@ -2075,14 +2126,17 @@ QJsonObject Room::Private::toJson() const QJsonObject {{ QStringLiteral("events"), stateEvents }}); } - QJsonArray accountDataEvents; if (!accountData.empty()) { + QJsonArray accountDataEvents; for (const auto& e: accountData) - appendEvent(accountDataEvents, e.first, e.second->contentJson()); + { + if (!e.second->contentJson().isEmpty()) + accountDataEvents.append(e.second->fullJson()); + } + result.insert(QStringLiteral("account_data"), + QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - result.insert(QStringLiteral("account_data"), - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, unreadMessages } }; @@ -2094,7 +2148,7 @@ QJsonObject Room::Private::toJson() const result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); - if (et.elapsed() > 50) + if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; return result; @@ -18,7 +18,7 @@ #pragma once -#include "jobs/syncjob.h" +#include "csapi/message_pagination.h" #include "events/roommessageevent.h" #include "events/accountdataevents.h" #include "eventitem.h" @@ -33,6 +33,7 @@ namespace QMatrixClient { class Event; + class SyncRoomData; class RoomMemberEvent; class Connection; class User; @@ -95,12 +96,32 @@ namespace QMatrixClient Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) + Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) + public: using Timeline = std::deque<TimelineItem>; using PendingEvents = std::vector<PendingEventItem>; using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; + enum Change : uint { + NoChange = 0x0, + NameChange = 0x1, + CanonicalAliasChange = 0x2, + TopicChange = 0x4, + UnreadNotifsChange = 0x8, + AvatarChange = 0x10, + JoinStateChange = 0x20, + TagsChange = 0x40, + MembersChange = 0x80, + EncryptionOn = 0x100, + AccountDataChange = 0x200, + OtherChange = 0x1000, + AnyChange = 0x1FFF + }; + Q_DECLARE_FLAGS(Changes, Change) + Q_FLAG(Changes) + Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; @@ -126,6 +147,8 @@ namespace QMatrixClient int timelineSize() const; bool usesEncryption() const; + GetRoomEventsJob* eventsHistoryJob() const; + /** * Returns a square room avatar with the given size and requests it * from the network if needed @@ -177,9 +200,16 @@ namespace QMatrixClient const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; /** - * A convenience method returning the read marker to - * the before-oldest message + * A convenience method returning the read marker to the position + * before the "oldest" event; same as messageEvents().crend() + */ + rev_iter_t historyEdge() const; + /** + * A convenience method returning the iterator beyond the latest + * arrived event; same as messageEvents().cend() */ + Timeline::const_iterator syncEdge() const; + /// \deprecated Use historyEdge instead rev_iter_t timelineEdge() const; Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; @@ -323,6 +353,7 @@ namespace QMatrixClient * * Takes ownership of the event, deleting it once the matching one * arrives with the sync + * \return transaction id associated with the event. */ QString postEvent(RoomEvent* event); QString postJson(const QString& matrixType, @@ -356,6 +387,7 @@ namespace QMatrixClient void markAllMessagesAsRead(); signals: + void eventsHistoryJobChanged(); void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); @@ -368,6 +400,13 @@ namespace QMatrixClient void pendingEventDiscarded(); void pendingEventChanged(int pendingEventIndex); + /** A common signal for various kinds of changes in the room + * Aside from all changes in the room state + * @param changes a set of flags describing what changes occured + * upon the last sync + * \sa StateChange + */ + void changed(Changes changes); /** * \brief The room name, the canonical alias or other aliases changed * @@ -417,27 +456,28 @@ namespace QMatrixClient /// The room is about to be deleted void beforeDestruction(Room*); - public: // Used by Connection - not a part of the client API - QJsonObject toJson() const; - void updateData(SyncRoomData&& data ); - - // Clients should use Connection::joinRoom() and Room::leaveRoom() - // to change the room state - void setJoinState( JoinState state ); - protected: /// Returns true if any of room names/aliases has changed - virtual bool processStateEvent(const RoomEvent& e); + virtual Changes processStateEvent(const RoomEvent& e); virtual void processEphemeralEvent(EventPtr&& event); - virtual void processAccountDataEvent(EventPtr&& event); + virtual Changes processAccountDataEvent(EventPtr&& event); virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } virtual void onRedaction(const RoomEvent& /*prevEvent*/, const RoomEvent& /*after*/) { } + virtual QJsonObject toJson() const; + virtual void updateData(SyncRoomData&& data, bool fromCache = false); private: + friend class Connection; + class Private; Private* d; + + // This is called from Connection, reflecting a state change that + // arrived from the server. Clients should use + // Connection::joinRoom() and Room::leaveRoom() to change the state. + void setJoinState(JoinState state); }; class MemberSorter @@ -460,3 +500,4 @@ namespace QMatrixClient }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) +Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp new file mode 100644 index 00000000..1023ed6a --- /dev/null +++ b/lib/syncdata.cpp @@ -0,0 +1,180 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "syncdata.h" + +#include "events/eventloader.h" + +#include <QtCore/QFile> +#include <QtCore/QFileInfo> + +using namespace QMatrixClient; + +const QString SyncRoomData::UnreadCountKey = + QStringLiteral("x-qmatrixclient.unread_count"); + +template <typename EventsArrayT, typename StrT> +inline EventsArrayT load(const QJsonObject& batches, StrT keyName) +{ + return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); +} + +SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, + const QJsonObject& room_) + : roomId(roomId_) + , joinState(joinState_) + , state(load<StateEvents>(room_, joinState == JoinState::Invite + ? "invite_state"_ls : "state"_ls)) +{ + switch (joinState) { + case JoinState::Join: + ephemeral = load<Events>(room_, "ephemeral"_ls); + FALLTHROUGH; + case JoinState::Leave: + { + accountData = load<Events>(room_, "account_data"_ls); + timeline = load<RoomEvents>(room_, "timeline"_ls); + const auto timelineJson = room_.value("timeline"_ls).toObject(); + timelineLimited = timelineJson.value("limited"_ls).toBool(); + timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); + + break; + } + default: /* nothing on top of state */; + } + + const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); + unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); + highlightCount = unreadJson.value("highlight_count"_ls).toInt(); + notificationCount = unreadJson.value("notification_count"_ls).toInt(); + if (highlightCount > 0 || notificationCount > 0) + qCDebug(SYNCJOB) << "Room" << roomId_ + << "has highlights:" << highlightCount + << "and notifications:" << notificationCount; +} + +SyncData::SyncData(const QString& cacheFileName) +{ + QFileInfo cacheFileInfo { cacheFileName }; + auto json = loadJson(cacheFileName); + auto requiredVersion = std::get<0>(cacheVersion()); + auto actualVersion = json.value("cache_version").toObject() + .value("major").toInt(); + if (actualVersion == requiredVersion) + parseJson(json, cacheFileInfo.absolutePath() + '/'); + else + qCWarning(MAIN) + << "Major version of the cache file is" << actualVersion << "but" + << requiredVersion << "is required; discarding the cache"; +} + +SyncDataList&& SyncData::takeRoomData() +{ + return move(roomData); +} + +QString SyncData::fileNameForRoom(QString roomId) +{ + roomId.replace(':', '_'); + return roomId + ".json"; +} + +Events&& SyncData::takePresenceData() +{ + return std::move(presenceData); +} + +Events&& SyncData::takeAccountData() +{ + return std::move(accountData); +} + +Events&& SyncData::takeToDeviceEvents() +{ + return std::move(toDeviceEvents); +} + +QJsonObject SyncData::loadJson(const QString& fileName) +{ + QFile roomFile { fileName }; + if (!roomFile.exists()) + { + qCWarning(MAIN) << "No state cache file" << fileName; + return {}; + } + if(!roomFile.open(QIODevice::ReadOnly)) + { + qCWarning(MAIN) << "Failed to open state cache file" + << roomFile.fileName(); + return {}; + } + auto data = roomFile.readAll(); + + const auto json = + (data.startsWith('{') ? QJsonDocument::fromJson(data) + : QJsonDocument::fromBinaryData(data)).object(); + if (json.isEmpty()) + { + qCWarning(MAIN) << "State cache in" << fileName + << "is broken or empty, discarding"; + } + return json; +} + +void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) +{ + QElapsedTimer et; et.start(); + + nextBatch_ = json.value("next_batch"_ls).toString(); + presenceData = load<Events>(json, "presence"_ls); + accountData = load<Events>(json, "account_data"_ls); + toDeviceEvents = load<Events>(json, "to_device"_ls); + + auto rooms = json.value("rooms"_ls).toObject(); + JoinStates::Int ii = 1; // ii is used to make a JoinState value + auto totalRooms = 0; + auto totalEvents = 0; + for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) + { + const auto rs = rooms.value(JoinStateStrings[i]).toObject(); + // We have a Qt container on the right and an STL one on the left + roomData.reserve(static_cast<size_t>(rs.size())); + for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) + { + auto roomJson = roomIt->isObject() + ? roomIt->toObject() + : loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) + { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); + const auto& r = roomData.back(); + totalEvents += r.state.size() + r.ephemeral.size() + + r.accountData.size() + r.timeline.size(); + } + totalRooms += rs.size(); + } + if (!unresolvedRoomIds.empty()) + qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(','); + if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" + << totalRooms << "room(s)," + << totalEvents << "event(s) in" << et; +} diff --git a/lib/syncdata.h b/lib/syncdata.h new file mode 100644 index 00000000..aa8948bc --- /dev/null +++ b/lib/syncdata.h @@ -0,0 +1,86 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "joinstate.h" +#include "events/stateevent.h" + +namespace QMatrixClient { + class SyncRoomData + { + public: + QString roomId; + JoinState joinState; + StateEvents state; + RoomEvents timeline; + Events ephemeral; + Events accountData; + + bool timelineLimited; + QString timelinePrevBatch; + int unreadCount; + int highlightCount; + int notificationCount; + + SyncRoomData(const QString& roomId, JoinState joinState_, + const QJsonObject& room_); + SyncRoomData(SyncRoomData&&) = default; + SyncRoomData& operator=(SyncRoomData&&) = default; + + static const QString UnreadCountKey; + }; + + // QVector cannot work with non-copiable objects, std::vector can. + using SyncDataList = std::vector<SyncRoomData>; + + class SyncData + { + public: + SyncData() = default; + explicit SyncData(const QString& cacheFileName); + /** Parse sync response into room events + * \param json response from /sync or a room state cache + * \return the list of rooms with missing cache files; always + * empty when parsing response from /sync + */ + void parseJson(const QJsonObject& json, const QString& baseDir = {}); + + Events&& takePresenceData(); + Events&& takeAccountData(); + Events&& takeToDeviceEvents(); + SyncDataList&& takeRoomData(); + + QString nextBatch() const { return nextBatch_; } + + QStringList unresolvedRooms() const { return unresolvedRoomIds; } + + static std::pair<int, int> cacheVersion() { return { 9, 0 }; } + static QString fileNameForRoom(QString roomId); + + private: + QString nextBatch_; + Events presenceData; + Events accountData; + Events toDeviceEvents; + SyncDataList roomData; + QStringList unresolvedRoomIds; + + static QJsonObject loadJson(const QString& fileName); + }; +} // namespace QMatrixClient diff --git a/lib/user.cpp b/lib/user.cpp index eec08ad9..eec41957 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -312,6 +312,11 @@ void User::unmarkIgnore() connection()->removeFromIgnoredUsers(this); } +bool User::isIgnored() const +{ + return connection()->isIgnored(this); +} + void User::Private::setAvatarOnServer(QString contentUri, User* q) { auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri); @@ -321,14 +326,16 @@ void User::Private::setAvatarOnServer(QString contentUri, User* q) QString User::displayname(const Room* room) const { - auto name = d->nameForRoom(room); - return name.isEmpty() ? d->userId : - room ? room->roomMembername(this) : name; + if (room) + return room->roomMembername(this); + + const auto name = d->nameForRoom(nullptr); + return name.isEmpty() ? d->userId : name; } QString User::fullName(const Room* room) const { - auto name = d->nameForRoom(room); + const auto name = d->nameForRoom(room); return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; } @@ -125,6 +125,8 @@ namespace QMatrixClient void ignore(); /** Remove the user from the ignore list */ void unmarkIgnore(); + /** Check whether the user is in ignore list */ + bool isIgnored() const; signals: void nameAboutToChange(QString newName, QString oldName, diff --git a/lib/util.cpp b/lib/util.cpp index 1773fcfe..af06013c 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -19,6 +19,9 @@ #include "util.h" #include <QtCore/QRegularExpression> +#include <QtCore/QStandardPaths> +#include <QtCore/QDir> +#include <QtCore/QStringBuilder> static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption @@ -61,3 +64,14 @@ QString QMatrixClient::prettyPrint(const QString& plainText) linkifyUrls(pt); return pt; } + +QString QMatrixClient::cacheLocation(const QString& dirName) +{ + const QString cachePath = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + % '/' % dirName % '/'; + QDir dir; + if (!dir.exists(cachePath)) + dir.mkpath(cachePath); + return cachePath; +} @@ -240,5 +240,11 @@ namespace QMatrixClient * This includes HTML escaping of <,>,",& and URLs linkification. */ QString prettyPrint(const QString& plainText); + + /** Return a path to cache directory after making sure that it exists + * The returned path has a trailing slash, clients don't need to append it. + * \param dir path to cache directory relative to the standard cache path + */ + QString cacheLocation(const QString& dirName); } // namespace QMatrixClient |