From 04435ef1d621b18ad837cdcb4b06155952f51f0c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 15:38:05 +0900 Subject: DEFINE_SIMPLE_STATE_EVENT: fix value_type mistakenly dubbed as content_type --- lib/events/simplestateevents.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 56be947c..9a212612 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -59,11 +59,11 @@ namespace QMatrixClient }; } // namespace EventContent -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public StateEvent> \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + class _Name : public StateEvent> \ { \ 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)) \ -- cgit v1.2.3 From f3221451ce29c3b571e08fe3ce51a49f252029e5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 16:06:29 +0900 Subject: DEFINE_SIMPLE_STATE_EVENT: fix construction from an rvalue QJsonObject --- lib/events/simplestateevents.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 9a212612..3a59ad6d 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -65,8 +65,9 @@ namespace QMatrixClient public: \ using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \ + explicit _Name(QJsonObject obj) \ + : StateEvent(typeId(), std::move(obj), \ + QStringLiteral(#_ContentKey)) \ { } \ template \ explicit _Name(T&& value) \ -- cgit v1.2.3 From 9993a0ea50165fd70f75b68c329ea045fb51d7f4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 16:07:53 +0900 Subject: Support dumping Events to QDebug --- lib/events/event.cpp | 5 +++++ lib/events/event.h | 10 ++++++++++ lib/events/stateevent.cpp | 8 ++++++++ lib/events/stateevent.h | 2 ++ 4 files changed, 25 insertions(+) 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..76e77cf6 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -257,8 +257,18 @@ namespace QMatrixClient return fromJson(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/stateevent.cpp b/lib/events/stateevent.cpp index fd5d2642..ea7533c5 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -28,3 +28,11 @@ bool StateEventBase::repeatsState() const const auto prevContentJson = unsignedJson().value(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } + +void StateEventBase::dumpTo(QDebug dbg) const +{ + 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..e499bdff 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -30,6 +30,8 @@ namespace QMatrixClient { ~StateEventBase() override = default; bool isStateEvent() const override { return true; } + void dumpTo(QDebug dbg) const override; + virtual bool repeatsState() const; }; using StateEventPtr = event_ptr_tt; -- cgit v1.2.3 From 2fe086f4e8f15cf366fc2cf1c9942c7b7541cec7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 17:53:53 +0900 Subject: StateEvent::dumpTo: add state_key to the logline --- lib/events/stateevent.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index ea7533c5..280c334c 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -31,6 +31,8 @@ bool StateEventBase::repeatsState() const void StateEventBase::dumpTo(QDebug dbg) const { + if (!stateKey().isEmpty()) + dbg << '<' << stateKey() << "> "; if (unsignedJson().contains(PrevContentKeyL)) dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) .toJson(QJsonDocument::Compact) << " -> "; -- cgit v1.2.3 From d4edc5eb4eec92a96fcaf4eefc59943dfb59e02e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 17:55:22 +0900 Subject: StateEventKey and std::hash to arrange state events in hashmaps --- lib/events/stateevent.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index e499bdff..76c749f5 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -37,6 +37,13 @@ namespace QMatrixClient { using StateEventPtr = event_ptr_tt; using StateEvents = EventsArray; + /** + * 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; + template struct Prev { @@ -92,3 +99,13 @@ namespace QMatrixClient { std::unique_ptr> _prev; }; } // namespace QMatrixClient + +namespace std { + template <> struct hash + { + size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT + { + return qHash(k); + } + }; +} -- cgit v1.2.3 From 6dd950637d0c90c7540cd64b2eb002f1414389a5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 18:01:49 +0900 Subject: Room: store state events in a unified way Closes #194. --- lib/room.cpp | 156 ++++++++++++++++++++++------------------------------------- 1 file changed, 59 insertions(+), 97 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index f8ed2721..aa1dfbf6 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -92,18 +92,17 @@ class Room::Private void updateDisplayname(); Connection* connection; + QString id; + JoinState joinState; + // The state of the room at timeline position before-0 + std::unordered_map baseState; + // The state of the room at timeline position after-maxTimelineIndex() + QHash currentState; Timeline timeline; PendingEvents unsyncedEvents; QHash 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; @@ -171,6 +170,15 @@ class Room::Private void getPreviousContent(int limit = 10); + template + const EventT* getCurrentState(QString stateKey = {}) const + { + static const EventT emptyEvent { QJsonObject{} }; + return static_cast( + currentState.value({EventT::typeId(), stateKey}, + &emptyEvent)); + } + bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && @@ -288,17 +296,17 @@ const Room::PendingEvents& Room::pendingEvents() const QString Room::name() const { - return d->name; + return d->getCurrentState()->name(); } QStringList Room::aliases() const { - return d->aliases; + return d->getCurrentState()->aliases(); } QString Room::canonicalAlias() const { - return d->canonicalAlias; + return d->getCurrentState()->alias(); } QString Room::displayName() const @@ -308,7 +316,7 @@ QString Room::displayName() const QString Room::topic() const { - return d->topic; + return d->getCurrentState()->topic(); } QString Room::avatarMediaId() const @@ -945,7 +953,7 @@ int Room::timelineSize() const bool Room::usesEncryption() const { - return !d->encryptionAlgorithm.isEmpty(); + return !d->getCurrentState()->algorithm().isEmpty(); } void Room::Private::insertMemberIntoMap(User *u) @@ -1088,8 +1096,12 @@ void Room::updateData(SyncRoomData&& data) if (!data.state.empty()) { et.restart(); - for (const auto& e: data.state) - emitNamesChanged |= processStateEvent(*e); + for (auto&& eptr: data.state) + { + const auto& evt = *eptr; + d->baseState[{evt.type(),evt.stateKey()}] = move(eptr); + emitNamesChanged |= processStateEvent(evt); + } qCDebug(PROFILER) << "*** Room::processStateEvents():" << data.state.size() << "event(s)," << et; @@ -1747,38 +1759,32 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) bool Room::processStateEvent(const RoomEvent& e) { + if (!e.isStateEvent()) + return false; + + d->currentState[{e.type(),e.stateKey()}] = + static_cast(&e); + if (!is(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; + , [] (const RoomNameEvent&) { return true; } - , [this] (const RoomAliasesEvent& evt) { - d->aliases = evt.aliases(); - qCDebug(MAIN) << "Room aliases updated:" << d->aliases; + , [] (const RoomAliasesEvent&) { return true; } , [this] (const RoomCanonicalAliasEvent& evt) { - d->canonicalAlias = evt.alias(); - if (!d->canonicalAlias.isEmpty()) - setObjectName(d->canonicalAlias); - qCDebug(MAIN) << "Room canonical alias updated:" - << d->canonicalAlias; + setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); return true; } - , [this] (const RoomTopicEvent& evt) { - d->topic = evt.topic(); - qCDebug(MAIN) << "Room topic updated:" << d->topic; + , [this] (const RoomTopicEvent&) { emit topicChanged(); return false; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) - { - qCDebug(MAIN) << "Room avatar URL updated:" - << evt.url().toString(); emit avatarChanged(); - } return false; } , [this] (const RoomMemberEvent& evt) { @@ -1819,10 +1825,7 @@ bool Room::processStateEvent(const RoomEvent& e) } return false; } - , [this] (const EncryptionEvent& evt) { - d->encryptionAlgorithm = evt.algorithm(); - qCDebug(MAIN) << "Encryption switched on in room" << id() - << "with algorithm" << d->encryptionAlgorithm; + , [this] (const EncryptionEvent&) { emit encryption(); return false; } @@ -1976,27 +1979,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); @@ -2015,34 +2020,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 -void appendEvent(QJsonArray& events, const EvtT& event) -{ - appendEvent(events, EvtT::matrixTypeId(), event.toJson()); -} - QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); @@ -2050,23 +2027,8 @@ 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) + stateEvents.append(evt->fullJson()); const auto stateObjName = joinState == JoinState::Invite ? QStringLiteral("invite_state") : QStringLiteral("state"); @@ -2074,14 +2036,14 @@ 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()); + 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 } }; -- cgit v1.2.3 From 23ebed25b79f4b6edf630546d7d9d571398a1640 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 19:55:20 +0900 Subject: Profiler logging fixes and improvements --- lib/jobs/syncjob.cpp | 9 +++++---- lib/logging.h | 11 +++++++++++ lib/room.cpp | 30 ++++++++++++++++++------------ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 9cbac71b..6baf388e 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -109,11 +109,12 @@ BaseJob::Status SyncData::parseJson(const QJsonDocument &data) totalEvents += r.state.size() + r.ephemeral.size() + r.accountData.size() + r.timeline.size(); } - totalRooms += roomData.size(); + totalRooms += rs.size(); } - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; + if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" + << totalRooms << "room(s)," + << totalEvents << "event(s) in" << et; return BaseJob::Success; } diff --git a/lib/logging.h b/lib/logging.h index 8dbfdf30..6c93ca79 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_MIN_MS + PROFILER_LOG_MIN_MS + #else + 200 + #endif + * 1000; + } } inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et) diff --git a/lib/room.cpp b/lib/room.cpp index aa1dfbf6..9cdacd91 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -416,7 +416,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) @@ -458,7 +458,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 @@ -1103,8 +1103,9 @@ void Room::updateData(SyncRoomData&& data) emitNamesChanged |= 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()) { @@ -1112,8 +1113,9 @@ void Room::updateData(SyncRoomData&& data) // 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; + if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" + << data.timeline.size() << "event(s)," << et; } if (emitNamesChanged) emit namesChanged(this); @@ -1123,7 +1125,8 @@ void Room::updateData(SyncRoomData&& data) { et.restart(); d->addNewMessageEvents(move(data.timeline)); - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; + if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; } for( auto&& ephemeralEvent: data.ephemeral ) processEphemeralEvent(move(ephemeralEvent)); @@ -1162,7 +1165,7 @@ 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 + // TODO, #133: Enqueue the job rather than immediately trigger it. auto call = connection->callApi(BackgroundRequest, id, pEvent->matrixType(), txnId, pEvent->contentJson()); Room::connect(call, &BaseJob::started, q, @@ -1844,15 +1847,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(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 @@ -1891,10 +1896,11 @@ 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; } } @@ -2055,7 +2061,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; -- cgit v1.2.3 From a81383549df4db8a487a847dca41900f3ab38c27 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 4 Nov 2018 20:09:03 +0900 Subject: profilerMinNsecs(): Fix a misnomer - it's PROFILER_LOG_USECS now - and document it --- CONTRIBUTING.md | 8 ++++++++ lib/logging.h | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d50dc157..6ee39eec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -237,6 +237,14 @@ We want the software to have decent performance for typical users. At the same t Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. Beware of premature optimization and have profiling data around before going into some hardcore optimization. +Speaking of profiling logs (see README.md on how to turn them on) - in order +to reduce small timespan logging spam, there's a default limit of at least +200 microseconds to log most operations with the PROFILER +(aka libqmatrixclient.profile.debug) logging category. You can override this +limit by passing the new value (in microseconds) in PROFILER_LOG_USECS to +the compiler. In the future, this parameter will be made changeable at runtime +_if_ needed. + ## How to check proposed changes before submitting them Checking the code on at least one configuration is essential; if you only have diff --git a/lib/logging.h b/lib/logging.h index 6c93ca79..a3a65887 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -69,11 +69,11 @@ namespace QMatrixClient inline qint64 profilerMinNsecs() { return - #ifdef PROFILER_LOG_MIN_MS - PROFILER_LOG_MIN_MS - #else +#ifdef PROFILER_LOG_USECS + PROFILER_LOG_USECS +#else 200 - #endif +#endif * 1000; } } -- cgit v1.2.3 From be7d25ed22abd07a254bfb8ff6c30de4fcc79e6a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 5 Nov 2018 07:54:29 +0900 Subject: isEchoEvent: check the pending event for ids, not the synced one Synced events always have their event ids, so checking those for event id renders most of the function useless (and returns an incorrect result). Closes #248. --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 9cdacd91..cd4d4dca 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1300,15 +1300,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) -- cgit v1.2.3 From e85137fca110de758f59cde2f6c6368090cf65c5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 14 Nov 2018 16:19:58 +0900 Subject: Room: ensure proper error signalling on event sending failures --- lib/room.cpp | 94 +++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index cd4d4dca..5dd244f2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -223,6 +223,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 auto requestSetState(const QString& stateKey, const EvT& event) @@ -1166,49 +1168,41 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) { auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. - auto call = connection->callApi(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; - } + if (auto call = connection->callApi(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; } @@ -1221,6 +1215,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(), -- cgit v1.2.3 From 6ca6dde46b9c72fc8833bc6fb81614fb705424f2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 11 Nov 2018 15:24:13 +0900 Subject: Improvements in comments - registerEventType(): comment the cryptic _ variable - Room::postEvent: document the return value - Room::Private: upgrade comments to doc-comments - even though in Private, they still are helpful to show hints in IDEs. - General cleanup --- lib/converters.h | 2 +- lib/events/event.h | 2 +- lib/room.cpp | 10 ++++++---- lib/room.h | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) 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.h b/lib/events/event.h index 76e77cf6..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(); - return _; + return _; // Only to facilitate usage in static initialisation } // === Event === diff --git a/lib/room.cpp b/lib/room.cpp index 5dd244f2..088d1d8e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -94,9 +94,11 @@ class Room::Private Connection* connection; QString id; JoinState joinState; - // The state of the room at timeline position before-0 + /// The state of the room at timeline position before-0 + /// \sa timelineBase std::unordered_map baseState; - // The state of the room at timeline position after-maxTimelineIndex() + /// The state of the room at timeline position after-maxTimelineIndex() + /// \sa Room::syncEdge QHash currentState; Timeline timeline; PendingEvents unsyncedEvents; @@ -156,8 +158,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 fileTransfers; const RoomMessageEvent* getEventWithFile(const QString& eventId) const; diff --git a/lib/room.h b/lib/room.h index f1566ac5..a9ed9647 100644 --- a/lib/room.h +++ b/lib/room.h @@ -323,6 +323,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, -- cgit v1.2.3 From 2a72a0ad0f8b4a0d24c9c2262917ff658ca5fec4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 11 Nov 2018 15:33:03 +0900 Subject: Room: historyEdge(), syncEdge, Private::timelineBase() Also: make moveEventsToTimeline() always put historical events from position -1 rather than 0 so that Private::baseState could always correspond to the before-0 position. --- lib/room.cpp | 22 ++++++++++++++++++---- lib/room.h | 11 +++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 088d1d8e..fd4add3b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -170,6 +170,9 @@ 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 @@ -525,11 +528,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(); @@ -1018,8 +1031,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; @@ -1040,7 +1054,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; } diff --git a/lib/room.h b/lib/room.h index a9ed9647..0a0eb878 100644 --- a/lib/room.h +++ b/lib/room.h @@ -177,9 +177,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; -- cgit v1.2.3 From 7ce14ccedc7a5239396e7662da1b2ba45195c271 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 14 Nov 2018 07:14:45 +0900 Subject: DEFINE_SIMPLE_STATE_EVENT: Add default constructor ...that creates an "empty" event, i.e. an event with content initialised by a default constructor (not all content types support this but those for simple events do). --- lib/events/simplestateevents.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 3a59ad6d..5aa24c15 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -65,16 +65,17 @@ namespace QMatrixClient public: \ using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(QJsonObject obj) \ - : StateEvent(typeId(), std::move(obj), \ - QStringLiteral(#_ContentKey)) \ - { } \ + explicit _Name() : _Name(value_type()) { } \ template \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), \ QStringLiteral(#_ContentKey), \ std::forward(value)) \ { } \ + explicit _Name(QJsonObject obj) \ + : StateEvent(typeId(), std::move(obj), \ + QStringLiteral(#_ContentKey)) \ + { } \ auto _ContentKey() const { return content().value; } \ }; \ REGISTER_EVENT_TYPE(_Name) \ -- cgit v1.2.3 From 3478e691df49b9c0938220db57b03a9c6fcbec8d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 14 Nov 2018 07:26:31 +0900 Subject: Room: fix incorrect handling of state event redactions Also: use Matrix type instead of internal type id in StateEventKey (Because internal type id maps to the library type system which will not discern between Unknown events and therefore will mix together events of different types in Room::Private::baseState/currentState. The Room code is updated accordingly (bonus: more asserts there).) Closes #255. --- lib/events/stateevent.h | 2 +- lib/room.cpp | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 76c749f5..d50500f2 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -42,7 +42,7 @@ namespace QMatrixClient { * of state in Matrix. * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events */ - using StateEventKey = std::pair; + using StateEventKey = std::pair; template struct Prev diff --git a/lib/room.cpp b/lib/room.cpp index fd4add3b..38a4157e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -178,10 +178,12 @@ class Room::Private template const EventT* getCurrentState(QString stateKey = {}) const { - static const EventT emptyEvent { QJsonObject{} }; - return static_cast( - currentState.value({EventT::typeId(), stateKey}, - &emptyEvent)); + 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(evt); } bool isEventNotable(const TimelineItem& ti) const @@ -1117,7 +1119,8 @@ void Room::updateData(SyncRoomData&& data) for (auto&& eptr: data.state) { const auto& evt = *eptr; - d->baseState[{evt.type(),evt.stateKey()}] = move(eptr); + Q_ASSERT(evt.isStateEvent()); + d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); emitNamesChanged |= processStateEvent(evt); } @@ -1628,11 +1631,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; } @@ -1791,7 +1809,7 @@ bool Room::processStateEvent(const RoomEvent& e) if (!e.isStateEvent()) return false; - d->currentState[{e.type(),e.stateKey()}] = + d->currentState[{e.matrixType(),e.stateKey()}] = static_cast(&e); if (!is(e)) qCDebug(EVENTS) << "Room state event:" << e; @@ -2060,7 +2078,10 @@ QJsonObject Room::Private::toJson() const QJsonArray stateEvents; for (const auto& evt: currentState) + { + Q_ASSERT(evt->isStateEvent()); stateEvents.append(evt->fullJson()); + } const auto stateObjName = joinState == JoinState::Invite ? QStringLiteral("invite_state") : QStringLiteral("state"); -- cgit v1.2.3 From c94ad527ed94a4c1ca368dc8c8c59e490b907649 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 17 Nov 2018 19:50:34 +0900 Subject: User::isIgnored() --- lib/user.cpp | 5 +++++ lib/user.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/lib/user.cpp b/lib/user.cpp index bfd23ae2..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(userId, contentUri); diff --git a/lib/user.h b/lib/user.h index 17f5625f..0023b44a 100644 --- a/lib/user.h +++ b/lib/user.h @@ -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, -- cgit v1.2.3 From 760c42bbb6027bfc6ebeb70a3a77608378d7c510 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 17 Nov 2018 19:52:27 +0900 Subject: StateEventBase::replacedState() Brings event id of the state event that was in effect before this one arrived. This key is not specced but it's used in the wild since forever. --- lib/events/stateevent.cpp | 5 +++++ lib/events/stateevent.h | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 280c334c..fd8079be 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -29,6 +29,11 @@ bool StateEventBase::repeatsState() const 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()) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index d50500f2..d4a7e8b3 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -30,6 +30,7 @@ namespace QMatrixClient { ~StateEventBase() override = default; bool isStateEvent() const override { return true; } + QString replacedState() const; void dumpTo(QDebug dbg) const override; virtual bool repeatsState() const; -- cgit v1.2.3 From 6f18091a48530399908fbc6ebcb0697bae970abb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 17 Nov 2018 20:40:17 +0900 Subject: Room::processStateEvent: process banning correctly Closes #258. --- lib/room.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 38a4157e..3718a54b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1860,10 +1860,12 @@ 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); -- cgit v1.2.3 From 06edc1033427ca96f03954d810aef33e5c940597 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 17 Nov 2018 21:55:32 +0900 Subject: Room: cleanup --- lib/room.cpp | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 3718a54b..0db124ee 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1675,45 +1675,37 @@ inline bool isRedaction(const RoomEventPtr& ep) void Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); + if (events.empty()) + return; // 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(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; 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; @@ -1727,7 +1719,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; @@ -2079,7 +2071,7 @@ QJsonObject Room::Private::toJson() const { QJsonArray stateEvents; - for (const auto& evt: currentState) + for (const auto* evt: currentState) { Q_ASSERT(evt->isStateEvent()); stateEvents.append(evt->fullJson()); -- cgit v1.2.3 From af0c8135afce32d9e06cc2446d9c675693d2c5fb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 18 Nov 2018 14:18:45 +0900 Subject: BaseJob::rawDataSample() A new recommended (and localisable) way of getting a piece of raw response to display next to error messages as "details". BaseJob::rawData() returns exactly the trimmed piece of data, no "truncated" suffix there anymore. --- lib/connection.cpp | 13 ++++--------- lib/connection.h | 6 +++--- lib/jobs/basejob.cpp | 19 +++++++++++++++---- lib/jobs/basejob.h | 14 +++++++++++++- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index df9fb112..6bda932a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -65,10 +65,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 +224,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 +301,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,10 +310,10 @@ 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()); }); } diff --git a/lib/connection.h b/lib/connection.h index b06fb143..20dade76 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -519,7 +519,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 * @@ -537,11 +537,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); 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 -- cgit v1.2.3 From e9d72c469b4c9a2246a086e4c47d80fe7d011179 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 18 Nov 2018 20:21:06 +0900 Subject: Room: profile addHistoricalMessageEvents (+cleanup) --- lib/room.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 0db124ee..6777732b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -435,7 +435,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") @@ -1747,7 +1747,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`) @@ -1772,6 +1772,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { + QElapsedTimer et; et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); @@ -1794,6 +1795,9 @@ 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) @@ -1831,8 +1835,7 @@ bool Room::processStateEvent(const RoomEvent& e) 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 ) { -- cgit v1.2.3 From 82c78b63cdd093853fd058740e7038e3c8a1cbbd Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 18 Nov 2018 20:18:10 +0900 Subject: Room: expose eventsHistoryJob as a Q_PROPERTY --- lib/room.cpp | 9 ++++++++- lib/room.h | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 6777732b..4bd96fc3 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" @@ -975,6 +974,11 @@ bool Room::usesEncryption() const return !d->getCurrentState()->algorithm().isEmpty(); } +GetRoomEventsJob* Room::eventsHistoryJob() const +{ + return d->eventsHistoryJob; +} + void Room::Private::insertMemberIntoMap(User *u) { const auto userName = u->name(q); @@ -1393,10 +1397,13 @@ void Room::Private::getPreviousContent(int limit) { eventsHistoryJob = connection->callApi(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); } } diff --git a/lib/room.h b/lib/room.h index 0a0eb878..633d19dd 100644 --- a/lib/room.h +++ b/lib/room.h @@ -19,6 +19,7 @@ #pragma once #include "jobs/syncjob.h" +#include "csapi/message_pagination.h" #include "events/roommessageevent.h" #include "events/accountdataevents.h" #include "eventitem.h" @@ -95,6 +96,8 @@ 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; using PendingEvents = std::vector; @@ -126,6 +129,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 @@ -364,6 +369,7 @@ namespace QMatrixClient void markAllMessagesAsRead(); signals: + void eventsHistoryJobChanged(); void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); -- cgit v1.2.3 From 06998c1406aab9943b62d6facb36cdee1cf52115 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 19 Nov 2018 09:18:53 +0900 Subject: Room: process new state events after applying redactions This was one more cause of #257 - the case when a redaction on a state event arrives in the same batch as the redacted event. --- lib/room.cpp | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 4bd96fc3..5faff271 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -192,7 +192,7 @@ class Room::Private is(*ti); } - void addNewMessageEvents(RoomEvents&& events); + bool addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline @@ -1135,24 +1135,15 @@ void Room::updateData(SyncRoomData&& data) 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); + emitNamesChanged |= d->addNewMessageEvents(move(data.timeline)); if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << data.timeline.size() << "event(s)," << et; } if (emitNamesChanged) emit namesChanged(this); d->updateDisplayname(); - if (!data.timeline.empty()) - { - et.restart(); - d->addNewMessageEvents(move(data.timeline)); - if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } for( auto&& ephemeralEvent: data.ephemeral ) processEphemeralEvent(move(ephemeralEvent)); @@ -1679,11 +1670,11 @@ inline bool isRedaction(const RoomEventPtr& ep) return is(*ep); } -void Room::Private::addNewMessageEvents(RoomEvents&& events) +bool Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return; + return false; // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. @@ -1708,6 +1699,15 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) // If the target event comes later, it comes already redacted. } + // 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. + bool emitNamesChanged = false; + for (const auto& eptr: events) + emitNamesChanged |= q->processStateEvent(*eptr); + auto timelineSize = timeline.size(); auto totalInserted = 0; for (auto it = events.begin(); it != events.end();) @@ -1775,6 +1775,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + return emitNamesChanged; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) -- cgit v1.2.3 From e1fdb33a4161b29d6df590ccea339d361d9fc4e8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 19 Nov 2018 09:22:27 +0900 Subject: Don't cache empty events; prepare for lazy-loading These two are intermingled in Room::addHistoricalMessageEvents because processing empty events found in a historical batch is no different from discovering (not lazy-loaded) members. --- lib/room.cpp | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 5faff271..656788cb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1784,14 +1784,25 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) 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 @@ -2085,7 +2096,15 @@ QJsonObject Room::Private::toJson() const for (const auto* evt: currentState) { Q_ASSERT(evt->isStateEvent()); - stateEvents.append(evt->fullJson()); + if ((evt->isRedacted() && !is(*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 ? @@ -2098,7 +2117,10 @@ QJsonObject Room::Private::toJson() const { QJsonArray accountDataEvents; for (const auto& e: accountData) - accountDataEvents.append(e.second->fullJson()); + { + if (!e.second->contentJson().isEmpty()) + accountDataEvents.append(e.second->fullJson()); + } result.insert(QStringLiteral("account_data"), QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } -- cgit v1.2.3 From dc3d6bd3b46ae7a9e8d9b9f62e50db982ef2b004 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 20 Nov 2018 13:24:40 +0900 Subject: Make SyncData more self-contained and prepare for cache splitting SyncData now resides in its own pair of files and is capable to load either from file or from JSON. There is also (yet untested) capability to load rooms from files if a file name stands is the value for a given room id. This allows to store the master cache file separately from cache files for each room, massively easing the problem of bulky accounts that can overflow the poor capacity of Qt's JSON engine. --- CMakeLists.txt | 1 + lib/connection.cpp | 50 ++++----------- lib/jobs/syncjob.cpp | 110 ++------------------------------- lib/jobs/syncjob.h | 48 +-------------- lib/room.cpp | 2 +- lib/room.h | 2 +- lib/syncdata.cpp | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/syncdata.h | 85 +++++++++++++++++++++++++ 8 files changed, 278 insertions(+), 191 deletions(-) create mode 100644 lib/syncdata.cpp create mode 100644 lib/syncdata.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e3eb600..49c5d8b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ set(libqmatrixclient_SRCS lib/room.cpp lib/user.cpp lib/avatar.cpp + lib/syncdata.cpp lib/settings.cpp lib/networksettings.cpp lib/converters.cpp diff --git a/lib/connection.cpp b/lib/connection.cpp index 6bda932a..8a451a79 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -39,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -1059,9 +1058,6 @@ 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::saveState(const QUrl &toFile) const { if (!d->cacheState) @@ -1091,6 +1087,8 @@ void Connection::saveState(const QUrl &toFile) const QJsonObject inviteRooms; for (const auto* i : roomMap()) // Pass on rooms in Leave state { + // TODO: instead of adding the room JSON add a file name and save + // the JSON to that file. if (i->joinState() == JoinState::Invite) inviteRooms.insert(i->id(), i->toJson()); else @@ -1123,8 +1121,8 @@ void Connection::saveState(const QUrl &toFile) const } QJsonObject versionObj; - versionObj.insert("major", CACHE_VERSION_MAJOR); - versionObj.insert("minor", CACHE_VERSION_MINOR); + versionObj.insert("major", SyncData::cacheVersion().first); + versionObj.insert("minor", SyncData::cacheVersion().second); rootObj.insert("cache_version", versionObj); QJsonDocument json { rootObj }; @@ -1142,42 +1140,20 @@ void Connection::loadState(const QUrl &fromFile) 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 { + fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() }; + 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); + // 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)); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 6baf388e..ef9b45dd 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -18,10 +18,6 @@ #include "syncjob.h" -#include "events/eventloader.h" - -#include - using namespace QMatrixClient; static size_t jobId = 0; @@ -46,111 +42,15 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, setMaxRetries(std::numeric_limits::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 -inline EventsArrayT load(const QJsonObject& batches, StrT keyName) -{ - return fromJson(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(json, "presence"_ls); - accountData = load(json, "account_data"_ls); - toDeviceEvents = load(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) + d.parseJson(data.object()); + if (d.unresolvedRooms().isEmpty()) { - 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(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 += rs.size(); + qCCritical(MAIN) << "Incomplete sync response, missing rooms:" + << d.unresolvedRooms().join(','); + return BaseJob::IncorrectResponseError; } - if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; return BaseJob::Success; } -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(room_, - joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) -{ - switch (joinState) { - case JoinState::Join: - ephemeral = load(room_, "ephemeral"_ls); - FALLTHROUGH; - case JoinState::Leave: - { - accountData = load(room_, "account_data"_ls); - timeline = load(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; - - 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/room.cpp b/lib/room.cpp index 656788cb..e5653258 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -45,10 +45,10 @@ #include "connection.h" #include "user.h" #include "converters.h" +#include "syncdata.h" #include #include // for efficient string concats (operator%) -#include #include #include #include diff --git a/lib/room.h b/lib/room.h index 633d19dd..b741e229 100644 --- a/lib/room.h +++ b/lib/room.h @@ -18,7 +18,6 @@ #pragma once -#include "jobs/syncjob.h" #include "csapi/message_pagination.h" #include "events/roommessageevent.h" #include "events/accountdataevents.h" @@ -34,6 +33,7 @@ namespace QMatrixClient { class Event; + class SyncRoomData; class RoomMemberEvent; class Connection; class User; diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp new file mode 100644 index 00000000..f0d55fd6 --- /dev/null +++ b/lib/syncdata.cpp @@ -0,0 +1,171 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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 + +using namespace QMatrixClient; + +const QString SyncRoomData::UnreadCountKey = + QStringLiteral("x-qmatrixclient.unread_count"); + +template +inline EventsArrayT load(const QJsonObject& batches, StrT keyName) +{ + return fromJson(batches[keyName].toObject().value("events"_ls)); +} + +SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, + const QJsonObject& room_) + : roomId(roomId_) + , joinState(joinState_) + , state(load(room_, joinState == JoinState::Invite + ? "invite_state"_ls : "state"_ls)) +{ + switch (joinState) { + case JoinState::Join: + ephemeral = load(room_, "ephemeral"_ls); + FALLTHROUGH; + case JoinState::Leave: + { + accountData = load(room_, "account_data"_ls); + timeline = load(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) +{ + parseJson(loadJson(cacheFileName)); +} + +SyncDataList&& SyncData::takeRoomData() +{ + return move(roomData); +} + +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 {}; + } + auto requiredVersion = std::get<0>(cacheVersion()); + auto actualVersion = json.value("cache_version").toObject() + .value("major").toInt(); + if (actualVersion < requiredVersion) + { + qCWarning(MAIN) + << "Major version of the cache file is" << actualVersion << "but" + << requiredVersion << "is required; discarding the cache"; + return {}; + } + return json; +} + +void SyncData::parseJson(const QJsonObject& json) +{ + QElapsedTimer et; et.start(); + + nextBatch_ = json.value("next_batch"_ls).toString(); + presenceData = load(json, "presence"_ls); + accountData = load(json, "account_data"_ls); + toDeviceEvents = load(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(rs.size())); + for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) + { + auto roomJson = roomIt->isString() ? loadJson(roomIt->toString()) + : roomIt->toObject(); + 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 (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..d8007db9 --- /dev/null +++ b/lib/syncdata.h @@ -0,0 +1,85 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral + * + * 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; + + 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); + + Events&& takePresenceData(); + Events&& takeAccountData(); + Events&& takeToDeviceEvents(); + SyncDataList&& takeRoomData(); + + QString nextBatch() const { return nextBatch_; } + + QStringList unresolvedRooms() const { return unresolvedRoomIds; } + + static std::pair cacheVersion() { return { 8, 0 }; } + + private: + QString nextBatch_; + Events presenceData; + Events accountData; + Events toDeviceEvents; + SyncDataList roomData; + QStringList unresolvedRoomIds; + + static QJsonObject loadJson(const QString& fileName); + }; +} // namespace QMatrixClient -- cgit v1.2.3 From 8e9e89846f75cae907bb9c2c6868dcb0f37896ae Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 20 Nov 2018 13:25:07 +0900 Subject: eventloader.h: drop unneeded #include --- lib/events/eventloader.h | 1 - 1 file changed, 1 deletion(-) 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 { -- cgit v1.2.3 From 92a504cd7255fd23c2ce4b8ffb4880e80dc1d839 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 20 Nov 2018 15:30:59 +0900 Subject: SyncJob::parseJson: fix a validation mistake --- lib/jobs/syncjob.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index ef9b45dd..ac0f6685 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -46,11 +46,10 @@ BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { d.parseJson(data.object()); if (d.unresolvedRooms().isEmpty()) - { - qCCritical(MAIN) << "Incomplete sync response, missing rooms:" - << d.unresolvedRooms().join(','); - return BaseJob::IncorrectResponseError; - } - return BaseJob::Success; + return BaseJob::Success; + + qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + << d.unresolvedRooms().join(','); + return BaseJob::IncorrectResponseError; } -- cgit v1.2.3 From f9dccac588f2aa1c809018c0c5eb606a1470d2c5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 20 Nov 2018 20:26:31 +0900 Subject: Add syncdata.* to libqmatrixclient.pri --- libqmatrixclient.pri | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index cb90a9fd..8ca43e56 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -17,6 +17,7 @@ HEADERS += \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ $$SRCPATH/avatar.h \ + $$SRCPATH/syncdata.h \ $$SRCPATH/util.h \ $$SRCPATH/events/event.h \ $$SRCPATH/events/roomevent.h \ @@ -60,6 +61,7 @@ SOURCES += \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ $$SRCPATH/avatar.cpp \ + $$SRCPATH/syncdata.cpp \ $$SRCPATH/util.cpp \ $$SRCPATH/events/event.cpp \ $$SRCPATH/events/roomevent.cpp \ -- cgit v1.2.3 From 7f6dec0676123629d2cdf9da7640c1e17566ed3d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 22 Nov 2018 09:31:10 +0900 Subject: Generalise and expose cacheLocation() --- lib/avatar.cpp | 12 +----------- lib/util.cpp | 14 ++++++++++++++ lib/util.h | 6 ++++++ 3 files changed, 21 insertions(+), 11 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/util.cpp b/lib/util.cpp index 1773fcfe..5266af1e 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -19,6 +19,9 @@ #include "util.h" #include +#include +#include +#include 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 auto cachePath = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + % '/' % dirName % '/'; + QDir dir; + if (!dir.exists(cachePath)) + dir.mkpath(cachePath); + return cachePath; +} diff --git a/lib/util.h b/lib/util.h index 13eec143..88c756a1 100644 --- a/lib/util.h +++ b/lib/util.h @@ -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 -- cgit v1.2.3 From 0c3a45356a803baa0eb5e553262a85cac897ac4f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 22 Nov 2018 13:00:33 +0900 Subject: Room: Change enum, Changes flag set, and changed() signal This allows to batch updates into signals being emitted only once per sync. Also supercedes emitNamesChanged flag used in a few places. --- lib/room.cpp | 49 +++++++++++++++++++++++++++---------------------- lib/room.h | 28 +++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index e5653258..55923ed8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -192,7 +192,7 @@ class Room::Private is(*ti); } - bool addNewMessageEvents(RoomEvents&& events); + Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline @@ -1116,7 +1116,7 @@ void Room::updateData(SyncRoomData&& data) for (auto&& event: data.accountData) processAccountDataEvent(move(event)); - bool emitNamesChanged = false; + Changes roomChanges = Change::NoChange; if (!data.state.empty()) { et.restart(); @@ -1125,7 +1125,7 @@ void Room::updateData(SyncRoomData&& data) const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); - emitNamesChanged |= processStateEvent(evt); + roomChanges |= processStateEvent(evt); } if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) @@ -1135,13 +1135,17 @@ void Room::updateData(SyncRoomData&& data) if (!data.timeline.empty()) { et.restart(); - emitNamesChanged |= d->addNewMessageEvents(move(data.timeline)); + 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(); for( auto&& ephemeralEvent: data.ephemeral ) @@ -1165,6 +1169,8 @@ void Room::updateData(SyncRoomData&& data) d->notificationCount = data.notificationCount; emit notificationCountChanged(this); } + if (roomChanges != Change::NoChange) + emit changed(roomChanges); } QString Room::Private::sendEvent(RoomEventPtr&& event) @@ -1670,11 +1676,11 @@ inline bool isRedaction(const RoomEventPtr& ep) return is(*ep); } -bool Room::Private::addNewMessageEvents(RoomEvents&& events) +Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return false; + return Change::NoChange; // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. @@ -1704,9 +1710,9 @@ bool Room::Private::addNewMessageEvents(RoomEvents&& events) // 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. - bool emitNamesChanged = false; + Changes stateChanges = Change::NoChange; for (const auto& eptr: events) - emitNamesChanged |= q->processStateEvent(*eptr); + stateChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); auto totalInserted = 0; @@ -1775,7 +1781,7 @@ bool Room::Private::addNewMessageEvents(RoomEvents&& events) } Q_ASSERT(timeline.size() == timelineSize + totalInserted); - return emitNamesChanged; + return stateChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1819,10 +1825,10 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) << insertedSize << "event(s)," << et; } -bool Room::processStateEvent(const RoomEvent& e) +Room::Changes Room::processStateEvent(const RoomEvent& e) { if (!e.isStateEvent()) - return false; + return Change::NoChange; d->currentState[{e.matrixType(),e.stateKey()}] = static_cast(&e); @@ -1831,23 +1837,22 @@ bool Room::processStateEvent(const RoomEvent& e) return visit(e , [] (const RoomNameEvent&) { - return true; + return NameChange; } , [] (const RoomAliasesEvent&) { - return true; + return OtherChange; } , [this] (const RoomCanonicalAliasEvent& evt) { setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); - return true; + return CanonicalAliasChange; } - , [this] (const RoomTopicEvent&) { - emit topicChanged(); - return false; + , [] (const RoomTopicEvent&) { + return TopicChange; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) emit avatarChanged(); - return false; + return AvatarChange; } , [this] (const RoomMemberEvent& evt) { auto* u = user(evt.userId()); @@ -1886,11 +1891,11 @@ bool Room::processStateEvent(const RoomEvent& e) emit userRemoved(u); } } - return false; + return MembersChange; } , [this] (const EncryptionEvent&) { - emit encryption(); - return false; + emit encryption(); // It can only be done once, so emit it here. + return EncryptionOn; } ); } diff --git a/lib/room.h b/lib/room.h index b741e229..ab8298d4 100644 --- a/lib/room.h +++ b/lib/room.h @@ -104,6 +104,24 @@ namespace QMatrixClient 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; @@ -382,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 * @@ -441,7 +466,7 @@ namespace QMatrixClient 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 void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } @@ -474,3 +499,4 @@ namespace QMatrixClient }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) +Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes) -- cgit v1.2.3 From 5fb74ca3d253b658fe77aaeb6a106cf6c0a9e7f0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 22 Nov 2018 16:51:49 +0900 Subject: Save state cache per-room Closes #257. --- lib/connection.cpp | 67 ++++++++++++++++++++++++++++++------------------------ lib/connection.h | 7 ++++-- lib/room.cpp | 3 +++ lib/syncdata.cpp | 37 ++++++++++++++++++------------ lib/syncdata.h | 5 ++-- 5 files changed, 70 insertions(+), 49 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 8a451a79..099d6a4e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1058,41 +1058,55 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -void Connection::saveState(const QUrl &toFile) const +void Connection::saveRoomState(Room* r) const { + Q_ASSERT(r); if (!d->cacheState) return; - QElapsedTimer et; et.start(); + 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()); + } else { + qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() + << ":" << outRoomFile.errorString(); + } +} - QFileInfo stateFile { - toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() - }; - if (!stateFile.dir().exists()) - stateFile.dir().mkpath("."); +void Connection::saveState() const +{ + if (!d->cacheState) + return; - QFile outfile { stateFile.absoluteFilePath() }; - if (!outfile.open(QFile::WriteOnly)) + QElapsedTimer et; et.start(); + + 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 { - // TODO: instead of adding the room JSON add a file name and save - // the JSON to that file. - if (i->joinState() == JoinState::Invite) - inviteRooms.insert(i->id(), i->toJson()); - else - rooms.insert(i->id(), i->toJson()); + auto& targetArray = i->joinState() == JoinState::Invite + ? inviteRooms : rooms; + targetArray.insert(i->id(), QJsonObject()); QElapsedTimer et1; et1.start(); QCoreApplication::processEvents(); if (et1.elapsed() > 1) @@ -1120,29 +1134,23 @@ void Connection::saveState(const QUrl &toFile) const QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - QJsonObject versionObj; - versionObj.insert("major", SyncData::cacheVersion().first); - versionObj.insert("minor", SyncData::cacheVersion().second); - 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(); - SyncData sync { - fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() }; + SyncData sync { stateCachePath() % "state.json" }; if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; @@ -1162,8 +1170,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 20dade76..20a1f47e 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 diff --git a/lib/room.cpp b/lib/room.cpp index 55923ed8..2d958dca 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1170,7 +1170,10 @@ void Room::updateData(SyncRoomData&& data) emit notificationCountChanged(this); } if (roomChanges != Change::NoChange) + { emit changed(roomChanges); + connection()->saveRoomState(this); + } } QString Room::Private::sendEvent(RoomEventPtr&& event) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index f0d55fd6..d141a7cc 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -21,6 +21,7 @@ #include "events/eventloader.h" #include +#include using namespace QMatrixClient; @@ -69,7 +70,17 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, SyncData::SyncData(const QString& cacheFileName) { - parseJson(loadJson(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() @@ -77,6 +88,12 @@ SyncDataList&& SyncData::takeRoomData() return move(roomData); } +QString SyncData::fileNameForRoom(QString roomId) +{ + roomId.replace(':', '_'); + return roomId + ".json"; +} + Events&& SyncData::takePresenceData() { return std::move(presenceData); @@ -115,22 +132,11 @@ QJsonObject SyncData::loadJson(const QString& fileName) { qCWarning(MAIN) << "State cache in" << fileName << "is broken or empty, discarding"; - return {}; - } - auto requiredVersion = std::get<0>(cacheVersion()); - auto actualVersion = json.value("cache_version").toObject() - .value("major").toInt(); - if (actualVersion < requiredVersion) - { - qCWarning(MAIN) - << "Major version of the cache file is" << actualVersion << "but" - << requiredVersion << "is required; discarding the cache"; - return {}; } return json; } -void SyncData::parseJson(const QJsonObject& json) +void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) { QElapsedTimer et; et.start(); @@ -150,8 +156,9 @@ void SyncData::parseJson(const QJsonObject& json) roomData.reserve(static_cast(rs.size())); for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { - auto roomJson = roomIt->isString() ? loadJson(roomIt->toString()) - : roomIt->toObject(); + auto roomJson = roomIt->isString() + ? loadJson(baseDir + fileNameForRoom(roomIt.key())) + : roomIt->toObject(); if (roomJson.isEmpty()) { unresolvedRoomIds.push_back(roomIt.key()); diff --git a/lib/syncdata.h b/lib/syncdata.h index d8007db9..aa8948bc 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -59,7 +59,7 @@ namespace QMatrixClient { * \return the list of rooms with missing cache files; always * empty when parsing response from /sync */ - void parseJson(const QJsonObject& json); + void parseJson(const QJsonObject& json, const QString& baseDir = {}); Events&& takePresenceData(); Events&& takeAccountData(); @@ -70,7 +70,8 @@ namespace QMatrixClient { QStringList unresolvedRooms() const { return unresolvedRoomIds; } - static std::pair cacheVersion() { return { 8, 0 }; } + static std::pair cacheVersion() { return { 9, 0 }; } + static QString fileNameForRoom(QString roomId); private: QString nextBatch_; -- cgit v1.2.3 From 7f50a504c7c4b266f98d0a0f449e3025e7a263ea Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 22 Nov 2018 21:06:03 +0900 Subject: Fix QString initialisation from QStringBuilder You can't assign a QStringBuilder to auto. --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 5266af1e..af06013c 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -67,7 +67,7 @@ QString QMatrixClient::prettyPrint(const QString& plainText) QString QMatrixClient::cacheLocation(const QString& dirName) { - const auto cachePath = + const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) % '/' % dirName % '/'; QDir dir; -- cgit v1.2.3 From 53f3fe79ef91bb5ba318b61b3a073c12409abc72 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 22 Nov 2018 21:06:36 +0900 Subject: Connection: Log when a room state cache is written --- lib/connection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 099d6a4e..7feeb075 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1071,6 +1071,7 @@ void Connection::saveRoomState(Room* r) const 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(); -- cgit v1.2.3 From 64799eaf667840c7f81d80810508d948f64f97d6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 15:38:06 +0900 Subject: Connection::saveState: use null instead of an empty object for a room placeholder Otherwise placeholder objects are confused with normal room JSON objects when loading from the cache. Closes #257 (again). --- lib/connection.cpp | 11 ++--------- lib/syncdata.cpp | 8 +++++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 7feeb075..53835a80 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1104,15 +1104,8 @@ void Connection::saveState() const QJsonObject rooms; QJsonObject inviteRooms; for (const auto* i : roomMap()) // Pass on rooms in Leave state - { - auto& targetArray = i->joinState() == JoinState::Invite - ? inviteRooms : rooms; - targetArray.insert(i->id(), QJsonObject()); - 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()) diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index d141a7cc..1023ed6a 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -156,9 +156,9 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) roomData.reserve(static_cast(rs.size())); for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { - auto roomJson = roomIt->isString() - ? loadJson(baseDir + fileNameForRoom(roomIt.key())) - : roomIt->toObject(); + auto roomJson = roomIt->isObject() + ? roomIt->toObject() + : loadJson(baseDir + fileNameForRoom(roomIt.key())); if (roomJson.isEmpty()) { unresolvedRoomIds.push_back(roomIt.key()); @@ -171,6 +171,8 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) } 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)," -- cgit v1.2.3 From 49ad563550ba9d2d03fc7a519ccb857a6d08791c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 15:38:59 +0900 Subject: Room/Connection: don't save the just loaded room cache --- lib/connection.cpp | 6 +++--- lib/connection.h | 2 +- lib/room.cpp | 5 +++-- lib/room.h | 17 +++++++++-------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 53835a80..9372acd5 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -316,7 +316,7 @@ void Connection::sync(int timeout) }); } -void Connection::onSyncSuccess(SyncData &&data) { +void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) { @@ -337,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); } @@ -1156,7 +1156,7 @@ void Connection::loadState() // 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)); + onSyncSuccess(std::move(sync), true); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } diff --git a/lib/connection.h b/lib/connection.h index 20a1f47e..32533b6e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -677,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/room.cpp b/lib/room.cpp index 2d958dca..22a0d585 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1106,7 +1106,7 @@ 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; @@ -1172,7 +1172,8 @@ void Room::updateData(SyncRoomData&& data) if (roomChanges != Change::NoChange) { emit changed(roomChanges); - connection()->saveRoomState(this); + if (!fromCache) + connection()->saveRoomState(this); } } diff --git a/lib/room.h b/lib/room.h index ab8298d4..480de6fe 100644 --- a/lib/room.h +++ b/lib/room.h @@ -456,14 +456,6 @@ 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 Changes processStateEvent(const RoomEvent& e); @@ -473,10 +465,19 @@ namespace QMatrixClient 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 -- cgit v1.2.3 From 52081fe91724e5f2a82c55f0e6230d8841dd859a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 15:53:16 +0900 Subject: Room: track more Changes --- lib/room.cpp | 9 ++++++--- lib/room.h | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 22a0d585..2a1d6f66 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -382,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); } @@ -1112,11 +1113,11 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) 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)); - Changes roomChanges = Change::NoChange; if (!data.state.empty()) { et.restart(); @@ -1973,7 +1974,7 @@ void Room::processEphemeralEvent(EventPtr&& event) } } -void Room::processAccountDataEvent(EventPtr&& event) +Room::Changes Room::processAccountDataEvent(EventPtr&& event) { if (auto* evt = eventCast(event)) d->setTags(evt->tags()); @@ -2000,7 +2001,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 &userlist) const diff --git a/lib/room.h b/lib/room.h index 480de6fe..9d4561e5 100644 --- a/lib/room.h +++ b/lib/room.h @@ -460,7 +460,7 @@ namespace QMatrixClient /// Returns true if any of room names/aliases has changed 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*/, -- cgit v1.2.3 From fd524590e3888ee5b4c0e25eb2138db4763dd0ec Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 16:40:10 +0900 Subject: Room::setLastReadEvent: save room state when updating own read marker --- lib/room.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/room.cpp b/lib/room.cpp index 2a1d6f66..8b81bfb2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -401,6 +401,7 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi(id, storedId); emit q->readMarkerMoved(eventId, storedId); + connection->saveRoomState(q); } } -- cgit v1.2.3 From bea4a7c81769c7e241478e4b0b29c62f389bc957 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 19:20:00 +0900 Subject: Update CS API --- lib/csapi/definitions/room_event_filter.cpp | 6 +++++ lib/csapi/definitions/room_event_filter.h | 4 +++ lib/csapi/kicking.h | 2 +- lib/csapi/read_markers.h | 2 +- lib/csapi/rooms.cpp | 40 ----------------------------- lib/csapi/rooms.h | 26 ------------------- 6 files changed, 12 insertions(+), 68 deletions(-) diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp index f6f1e5cb..8cd2ded7 100644 --- a/lib/csapi/definitions/room_event_filter.cpp +++ b/lib/csapi/definitions/room_event_filter.cpp @@ -12,6 +12,8 @@ QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod) addParam(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam(jo, QStringLiteral("rooms"), pod.rooms); addParam(jo, QStringLiteral("contains_url"), pod.containsUrl); + addParam(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); + addParam(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); return jo; } @@ -24,6 +26,10 @@ RoomEventFilter FromJsonObject::operator()(const QJsonObject& j fromJson(jo.value("rooms"_ls)); result.containsUrl = fromJson(jo.value("contains_url"_ls)); + result.lazyLoadMembers = + fromJson(jo.value("lazy_load_members"_ls)); + result.includeRedundantMembers = + fromJson(jo.value("include_redundant_members"_ls)); return result; } diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 697fe661..87f01189 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -21,6 +21,10 @@ namespace QMatrixClient QStringList rooms; /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``. bool containsUrl; + /// If ``true``, the only ``m.room.member`` events returned in the ``state`` section of the ``/sync`` response are those which are definitely necessary for a client to display the ``sender`` of the timeline events in that response. If ``false``, ``m.room.member`` events are not filtered. By default, servers should suppress duplicate redundant lazy-loaded ``m.room.member`` events from being sent to a given client across multiple calls to ``/sync``, given that most clients cache membership events (see include_redundant_members to change this behaviour). + bool lazyLoadMembers; + /// If ``true``, the ``state`` section of the ``/sync`` response will always contain the ``m.room.member`` events required to display the ``sender`` of the timeline events in that response, assuming ``lazy_load_members`` is enabled. This means that redundant duplicate member events may be returned across multiple calls to ``/sync``. This is useful for naive clients who never track membership data. If ``false``, duplicate ``m.room.member`` events may be suppressed by the server across multiple calls to ``/sync``. If ``lazy_load_members`` is ``false`` this field is ignored. + bool includeRedundantMembers; }; QJsonObject toJson(const RoomEventFilter& pod); diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 5968187e..714079cf 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -29,7 +29,7 @@ namespace QMatrixClient * \param userId * The fully qualified user ID of the user being kicked. * \param reason - * The reason the user has been kicked. This will be supplied as the + * The reason the user has been kicked. This will be supplied as the * ``reason`` on the target's updated `m.room.member`_ event. */ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index f19f46b0..d982b477 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -26,7 +26,7 @@ namespace QMatrixClient * event MUST belong to the room. * \param mRead * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:domain.com`` + * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` * and is provided here to save that extra call. */ explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {}); diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 3befeee5..cebb295a 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -46,12 +46,6 @@ BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) return Success; } -class GetRoomStateWithKeyJob::Private -{ - public: - StateEventPtr data; -}; - QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -63,29 +57,9 @@ static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithK GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName, basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) - , d(new Private) -{ -} - -GetRoomStateWithKeyJob::~GetRoomStateWithKeyJob() = default; - -StateEventPtr&& GetRoomStateWithKeyJob::data() { - return std::move(d->data); -} - -BaseJob::Status GetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson(data); - return Success; } -class GetRoomStateByTypeJob::Private -{ - public: - StateEventPtr data; -}; - QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -97,21 +71,7 @@ static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByType GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType) : BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName, basePath % "/rooms/" % roomId % "/state/" % eventType) - , d(new Private) -{ -} - -GetRoomStateByTypeJob::~GetRoomStateByTypeJob() = default; - -StateEventPtr&& GetRoomStateByTypeJob::data() { - return std::move(d->data); -} - -BaseJob::Status GetRoomStateByTypeJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson(data); - return Success; } class GetRoomStateJob::Private diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 2366918b..80895b4e 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -80,19 +80,6 @@ namespace QMatrixClient */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey); - ~GetRoomStateWithKeyJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; }; /// Get the state identified by the type, with the empty state key. @@ -122,19 +109,6 @@ namespace QMatrixClient */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType); - ~GetRoomStateByTypeJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; }; /// Get all state events in the current state of a room. -- cgit v1.2.3 From 3fc5e927de395ebd949af742d2116367e6a01712 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 3 Dec 2018 17:53:51 -0500 Subject: use the configured paths in the pkgconfig file --- QMatrixClient.pc.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/QMatrixClient.pc.in b/QMatrixClient.pc.in index d2938ab7..efb41498 100644 --- a/QMatrixClient.pc.in +++ b/QMatrixClient.pc.in @@ -1,7 +1,7 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -includedir=${prefix}/include -libdir=${prefix}/lib +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ Name: QMatrixClient Description: A Qt5 library to write cross-platfrom clients for Matrix -- cgit v1.2.3 From 147115e7591b87086141418ead8e0e1237362f9c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 3 Dec 2018 18:08:35 -0500 Subject: use the API version as the SOVERSION --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e3eb600..5a1950b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,7 +141,7 @@ add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS}) set(API_VERSION "0.4") set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.0") -set_property(TARGET QMatrixClient PROPERTY SOVERSION 0 ) +set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} ) set_property(TARGET QMatrixClient PROPERTY INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) set_property(TARGET QMatrixClient APPEND PROPERTY -- cgit v1.2.3 From c665883be52016be51f5b0a902e43885b024a8ac Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 6 Dec 2018 20:57:37 +0900 Subject: Connection: Avoid Omittable<>::operator bool It was accidentally (and incorrectly) used in tags sorting code; will be dropped from Omittable<> in a later commit. --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 9372acd5..26c33767 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -825,7 +825,7 @@ QHash> Connection::tagsToRooms() const for (auto it = result.begin(); it != result.end(); ++it) std::sort(it->begin(), it->end(), [t=it.key()] (Room* r1, Room* r2) { - return r1->tags().value(t).order < r2->tags().value(t).order; + return r1->tags().value(t) < r2->tags().value(t); }); return result; } -- cgit v1.2.3 From be4a16cd4188ebeeba60768deadd88de5cc5be7b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 6 Dec 2018 21:23:07 +0900 Subject: function_traits<>: support any arity; add compile-time tests --- lib/util.cpp | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/util.h | 68 ++++++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/lib/util.cpp b/lib/util.cpp index af06013c..4c176fc7 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -75,3 +75,67 @@ QString QMatrixClient::cacheLocation(const QString& dirName) dir.mkpath(cachePath); return cachePath; } + +// Tests for function_traits<> + +#ifdef Q_CC_CLANG +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCSimplifyInspection" +#endif +using namespace QMatrixClient; + +int f(); +static_assert(std::is_same, int>::value, + "Test fn_return_t<>"); + +void f1(int); +static_assert(function_traits::arg_number == 1, + "Test fn_arg_number"); + +void f2(int, QString); +static_assert(std::is_same, QString>::value, + "Test fn_arg_t<>"); + +struct S { int mf(); }; +static_assert(is_callable_v, "Test member function"); +static_assert(returns(), "Test returns<> with member function"); + +struct Fo { void operator()(int); }; +static_assert(function_traits::arg_number == 1, "Test function object 1"); +static_assert(is_callable_v, "Test is_callable<>"); +static_assert(std::is_same, int>(), + "Test fn_arg_t defaulting to first argument"); + +static auto l = [] { return 1; }; +static_assert(is_callable_v, "Test is_callable_v<> with lambda"); +static_assert(std::is_same, int>::value, + "Test fn_return_t<> with lambda"); + +template +struct fn_object +{ + static int smf(double) { return 0; } +}; +template <> +struct fn_object +{ + void operator()(QString); +}; +static_assert(is_callable_v>, "Test function object"); +static_assert(returns>(), + "Test returns<> with function object"); +static_assert(!is_callable_v>, "Test non-function object"); +// FIXME: These two don't work +//static_assert(is_callable_v::smf)>, +// "Test static member function"); +//static_assert(returns::smf)>(), +// "Test returns<> with static member function"); + +template +QString ft(T&&); +static_assert(std::is_same)>, QString&&>(), + "Test function templates"); + +#ifdef Q_CC_CLANG +#pragma clang diagnostic pop +#endif diff --git a/lib/util.h b/lib/util.h index 88c756a1..3f5bcb5f 100644 --- a/lib/util.h +++ b/lib/util.h @@ -119,41 +119,69 @@ namespace QMatrixClient bool _omitted = false; }; + namespace _impl { + template struct fn_traits; + } + /** Determine traits of an arbitrary function/lambda/functor - * This only works with arity of 1 (1-argument) for now but is extendable - * to other cases. Also, doesn't work with generic lambdas and function - * objects that have operator() overloaded + * Doesn't work with generic lambdas and function objects that have + * operator() overloaded. * \sa https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 */ template - struct function_traits : public function_traits - { }; // A generic function object that has (non-overloaded) operator() + struct function_traits : public _impl::fn_traits {}; // Specialisation for a function - template - struct function_traits + template + struct function_traits { + static constexpr auto is_callable = true; using return_type = ReturnT; - using arg_type = ArgT; + using arg_types = std::tuple; + static constexpr auto arg_number = std::tuple_size::value - 1; }; - // Specialisation for a member function - template - struct function_traits - : function_traits - { }; + namespace _impl { + template + struct fn_traits + { + static constexpr auto is_callable = false; + }; + + template + struct fn_traits + : public fn_traits + { }; // A generic function object that has (non-overloaded) operator() - // Specialisation for a const member function - template - struct function_traits - : function_traits - { }; + // Specialisation for a member function + template + struct fn_traits + : function_traits + { }; + + // Specialisation for a const member function + template + struct fn_traits + : function_traits + { }; + } // namespace _impl template using fn_return_t = typename function_traits::return_type; - template - using fn_arg_t = typename function_traits::arg_type; + template + using fn_arg_t = + std::tuple_element_t::arg_types>; + + template + constexpr bool returns() + { + return std::is_same, R>::value; + } + + // Poor-man's is_invokable + template + constexpr auto is_callable_v = function_traits::is_callable; inline auto operator"" _ls(const char* s, std::size_t size) { -- cgit v1.2.3 From 1de8d511251163ed35e0647c70c3e94e071b2fe0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 6 Dec 2018 19:12:28 +0900 Subject: Special-case FALLTHROUGH for Clang --- lib/util.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/util.h b/lib/util.h index 3f5bcb5f..6b5c89e6 100644 --- a/lib/util.h +++ b/lib/util.h @@ -31,6 +31,8 @@ #define FALLTHROUGH [[fallthrough]] #elif __has_cpp_attribute(clang::fallthrough) #define FALLTHROUGH [[clang::fallthrough]] +#elif __has_cpp_attribute(gnu::fallthrough) +#define FALLTHROUGH [[gnu::fallthrough]] #else #define FALLTHROUGH // -fallthrough #endif -- cgit v1.2.3 From 1414f3b5cc2bca3a3871bfe569510c1a422629fe Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 15:24:45 +0900 Subject: RoomMemberEvent: cleanup Don't make JSON for event content only to parse it again; drop extraneous constructs. --- lib/events/roommemberevent.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index db25d026..149d74f8 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -29,13 +29,10 @@ namespace QMatrixClient enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, Undefined }; - explicit MemberEventContent(MembershipType mt = MembershipType::Join) + explicit MemberEventContent(MembershipType mt = Join) : membership(mt) { } explicit MemberEventContent(const QJsonObject& json); - explicit MemberEventContent(const QJsonValue& jv) - : MemberEventContent(jv.toObject()) - { } MembershipType membership; bool isDirect = false; @@ -60,7 +57,7 @@ namespace QMatrixClient : StateEvent(typeId(), obj) { } RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c.toJson()) + : StateEvent(typeId(), matrixTypeId(), c) { } // This is a special constructor enabling RoomMemberEvent to be -- cgit v1.2.3 From ced7179117fd67cb1632f943f4ba1fde96423c0c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 15:26:35 +0900 Subject: StateEvent<>: make data members private Keeping them protected extends API surface with no reasonable use from it (and for now derived classes don't access StateEvent<> data members directly, anyway). --- lib/events/stateevent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index d4a7e8b3..d82de7e1 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -95,7 +95,7 @@ namespace QMatrixClient { QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } - protected: + private: ContentT _content; std::unique_ptr> _prev; }; -- cgit v1.2.3 From ed1f15151babee9ebc690ffa5c2593119540e8f0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 19:18:51 +0900 Subject: Omittable: make operator-> and operator* return an empty object if omitted == true That is, instead of Q_ASSERTing in debug builds (release builds already work that way). The idea is that since the value is default-initialised anyway it can be used as a "blank canvas" to access specific fields inside the value's structure. The next commit will use that. --- lib/util.h | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/util.h b/lib/util.h index 6b5c89e6..1028e059 100644 --- a/lib/util.h +++ b/lib/util.h @@ -88,17 +88,19 @@ namespace QMatrixClient static_assert(!std::is_reference::value, "You cannot make an Omittable<> with a reference type"); public: + using value_type = std::decay_t; + explicit Omittable() : Omittable(none) { } - Omittable(NoneTag) : _value(std::decay_t()), _omitted(true) { } - Omittable(const std::decay_t& val) : _value(val) { } - Omittable(std::decay_t&& val) : _value(std::move(val)) { } - Omittable& operator=(const std::decay_t& val) + Omittable(NoneTag) : _value(value_type()), _omitted(true) { } + Omittable(const value_type& val) : _value(val) { } + Omittable(value_type&& val) : _value(std::move(val)) { } + Omittable& operator=(const value_type& val) { _value = val; _omitted = false; return *this; } - Omittable& operator=(std::decay_t&& val) + Omittable& operator=(value_type&& val) { _value = std::move(val); _omitted = false; @@ -106,15 +108,15 @@ namespace QMatrixClient } bool omitted() const { return _omitted; } - const std::decay_t& value() const { Q_ASSERT(!_omitted); return _value; } - std::decay_t& value() { Q_ASSERT(!_omitted); return _value; } - std::decay_t&& release() { _omitted = true; return std::move(_value); } + const value_type& value() const { Q_ASSERT(!_omitted); return _value; } + value_type& value() { Q_ASSERT(!_omitted); return _value; } + value_type&& release() { _omitted = true; return std::move(_value); } operator bool() const { return !omitted(); } - const std::decay* operator->() const { return &value(); } - std::decay_t* operator->() { return &value(); } - const std::decay_t& operator*() const { return value(); } - std::decay_t& operator*() { return value(); } + const value_type* operator->() const { return &_value; } + value_type* operator->() { return &_value; } + const value_type& operator*() const { return _value; } + value_type& operator*() { return _value; } private: T _value; -- cgit v1.2.3 From 5ea115d6eb0b60dfd0c2be5fbe5e69615b133238 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 24 Nov 2018 11:58:43 +0900 Subject: Omittable: better editability; drop implicit cast to bool --- lib/util.h | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/util.h b/lib/util.h index 1028e059..722a7e3d 100644 --- a/lib/util.h +++ b/lib/util.h @@ -108,15 +108,23 @@ namespace QMatrixClient } bool omitted() const { return _omitted; } - const value_type& value() const { Q_ASSERT(!_omitted); return _value; } - value_type& value() { Q_ASSERT(!_omitted); return _value; } + const value_type& value() const + { + Q_ASSERT(!_omitted); + return _value; + } + value_type& editValue() + { + _omitted = false; + return _value; + } value_type&& release() { _omitted = true; return std::move(_value); } - operator bool() const { return !omitted(); } - const value_type* operator->() const { return &_value; } - value_type* operator->() { return &_value; } - const value_type& operator*() const { return _value; } - value_type& operator*() { return _value; } + operator value_type&() & { return editValue(); } + const value_type* operator->() const & { return &value(); } + value_type* operator->() & { return &editValue(); } + const value_type& operator*() const & { return value(); } + value_type& operator*() & { return editValue(); } private: T _value; -- cgit v1.2.3 From 95d4df58b39962f771885a6615efe1a682aab356 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 20:11:35 +0900 Subject: function_traits: more tests, fix function objects/lambdas not working with some compilers A member function reference is not the same as a member function pointer. --- lib/util.cpp | 14 ++++++++++---- lib/util.h | 8 ++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/util.cpp b/lib/util.cpp index 4c176fc7..5e7644a1 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -100,10 +100,16 @@ struct S { int mf(); }; static_assert(is_callable_v, "Test member function"); static_assert(returns(), "Test returns<> with member function"); -struct Fo { void operator()(int); }; -static_assert(function_traits::arg_number == 1, "Test function object 1"); -static_assert(is_callable_v, "Test is_callable<>"); -static_assert(std::is_same, int>(), +struct Fo { int operator()(); }; +static_assert(is_callable_v, "Test is_callable<> with function object"); +static_assert(function_traits::arg_number == 0, "Test function object"); +static_assert(std::is_same, int>::value, + "Test return type of function object"); + +struct Fo1 { void operator()(int); }; +static_assert(function_traits::arg_number == 1, "Test function object 1"); +static_assert(is_callable_v, "Test is_callable<> with function object 1"); +static_assert(std::is_same, int>(), "Test fn_arg_t defaulting to first argument"); static auto l = [] { return 1; }; diff --git a/lib/util.h b/lib/util.h index 722a7e3d..0066c03d 100644 --- a/lib/util.h +++ b/lib/util.h @@ -149,8 +149,8 @@ namespace QMatrixClient { static constexpr auto is_callable = true; using return_type = ReturnT; - using arg_types = std::tuple; - static constexpr auto arg_number = std::tuple_size::value - 1; + using arg_types = std::tuple; + static constexpr auto arg_number = std::tuple_size::value; }; namespace _impl { @@ -161,8 +161,8 @@ namespace QMatrixClient }; template - struct fn_traits - : public fn_traits + struct fn_traits + : public fn_traits { }; // A generic function object that has (non-overloaded) operator() // Specialisation for a member function -- cgit v1.2.3 From 3392e66fd015e191b01f6e3fc6839edc3948e31f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 15:36:04 +0900 Subject: Refactor toJson/fillJson Both now use through a common JsonConverter<> template class with its base definition tuned for structs/QJsonObjects and specialisations for non-object types. This new implementation doesn't work with virtual fillJson functions yet (so EventContent classes still use toJson as a member function) and does not cope quite well with non-constructible objects (you have to specialise JsonConverter<> rather than, more intuitively, JsonObjectConverter<>), but overall is more streamlined compared to the previous implementation. It also fixes one important issue that pushed for a rewrite: the previous implementation was not working with structure hierarchies at all so (in particular) the Filter part of CS API was totally disfunctional. --- lib/application-service/definitions/location.cpp | 20 +- lib/application-service/definitions/location.h | 8 +- lib/application-service/definitions/protocol.cpp | 66 ++--- lib/application-service/definitions/protocol.h | 24 +- lib/application-service/definitions/user.cpp | 20 +- lib/application-service/definitions/user.h | 8 +- lib/converters.cpp | 38 ++- lib/converters.h | 288 ++++++++++----------- lib/csapi/admin.cpp | 40 +-- lib/csapi/administrative_contact.cpp | 40 ++- lib/csapi/content-repo.cpp | 8 +- lib/csapi/create_room.cpp | 32 +-- lib/csapi/definitions/auth_data.cpp | 19 +- lib/csapi/definitions/auth_data.h | 8 +- lib/csapi/definitions/client_device.cpp | 23 +- lib/csapi/definitions/client_device.h | 8 +- lib/csapi/definitions/device_keys.cpp | 26 +- lib/csapi/definitions/device_keys.h | 8 +- lib/csapi/definitions/event_filter.cpp | 26 +- lib/csapi/definitions/event_filter.h | 8 +- lib/csapi/definitions/public_rooms_response.cpp | 61 ++--- lib/csapi/definitions/public_rooms_response.h | 16 +- lib/csapi/definitions/push_condition.cpp | 23 +- lib/csapi/definitions/push_condition.h | 8 +- lib/csapi/definitions/push_rule.cpp | 29 +-- lib/csapi/definitions/push_rule.h | 8 +- lib/csapi/definitions/push_ruleset.cpp | 26 +- lib/csapi/definitions/push_ruleset.h | 8 +- lib/csapi/definitions/room_event_filter.cpp | 28 +- lib/csapi/definitions/room_event_filter.h | 12 +- lib/csapi/definitions/sync_filter.cpp | 74 +++--- lib/csapi/definitions/sync_filter.h | 49 +++- lib/csapi/definitions/user_identifier.cpp | 16 +- lib/csapi/definitions/user_identifier.h | 8 +- lib/csapi/definitions/wellknown/homeserver.cpp | 14 +- lib/csapi/definitions/wellknown/homeserver.h | 8 +- .../definitions/wellknown/identity_server.cpp | 14 +- lib/csapi/definitions/wellknown/identity_server.h | 8 +- lib/csapi/device_management.cpp | 4 +- lib/csapi/directory.cpp | 4 +- lib/csapi/event_context.cpp | 12 +- lib/csapi/filter.cpp | 4 +- lib/csapi/joining.cpp | 51 ++-- lib/csapi/keys.cpp | 35 +-- lib/csapi/list_joined_rooms.cpp | 2 +- lib/csapi/list_public_rooms.cpp | 17 +- lib/csapi/login.cpp | 20 +- lib/csapi/message_pagination.cpp | 6 +- lib/csapi/notifications.cpp | 29 +-- lib/csapi/openid.cpp | 8 +- lib/csapi/peeking_events.cpp | 6 +- lib/csapi/presence.cpp | 10 +- lib/csapi/profile.cpp | 8 +- lib/csapi/pusher.cpp | 59 ++--- lib/csapi/pushrules.cpp | 8 +- lib/csapi/redaction.cpp | 2 +- lib/csapi/registration.cpp | 18 +- lib/csapi/room_send.cpp | 2 +- lib/csapi/room_state.cpp | 4 +- lib/csapi/rooms.cpp | 40 +-- lib/csapi/rooms.h | 13 +- lib/csapi/search.cpp | 179 ++++++------- lib/csapi/tags.cpp | 14 +- lib/csapi/third_party_lookup.cpp | 12 +- lib/csapi/users.cpp | 20 +- lib/csapi/versions.cpp | 2 +- lib/csapi/voip.cpp | 2 +- lib/csapi/wellknown.cpp | 4 +- lib/csapi/whoami.cpp | 2 +- lib/csapi/{{base}}.cpp.mustache | 69 ++--- lib/csapi/{{base}}.h.mustache | 13 +- lib/events/accountdataevents.h | 35 +-- lib/events/eventloader.h | 10 +- lib/events/roommemberevent.cpp | 12 +- .../definitions/request_email_validation.cpp | 23 +- .../definitions/request_email_validation.h | 8 +- .../definitions/request_msisdn_validation.cpp | 26 +- .../definitions/request_msisdn_validation.h | 8 +- lib/identity/definitions/sid.cpp | 14 +- lib/identity/definitions/sid.h | 8 +- 80 files changed, 851 insertions(+), 1100 deletions(-) diff --git a/lib/application-service/definitions/location.cpp b/lib/application-service/definitions/location.cpp index 958a55bf..a53db8d7 100644 --- a/lib/application-service/definitions/location.cpp +++ b/lib/application-service/definitions/location.cpp @@ -6,25 +6,19 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const ThirdPartyLocation& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const ThirdPartyLocation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("alias"), pod.alias); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; } -ThirdPartyLocation FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, ThirdPartyLocation& result) { - ThirdPartyLocation result; - result.alias = - fromJson(jo.value("alias"_ls)); - result.protocol = - fromJson(jo.value("protocol"_ls)); - result.fields = - fromJson(jo.value("fields"_ls)); - - return result; + fromJson(jo.value("alias"_ls), result.alias); + fromJson(jo.value("protocol"_ls), result.protocol); + fromJson(jo.value("fields"_ls), result.fields); } diff --git a/lib/application-service/definitions/location.h b/lib/application-service/definitions/location.h index 89b48a43..5586cfc6 100644 --- a/lib/application-service/definitions/location.h +++ b/lib/application-service/definitions/location.h @@ -21,12 +21,10 @@ namespace QMatrixClient /// Information used to identify this third party location. QJsonObject fields; }; - - QJsonObject toJson(const ThirdPartyLocation& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - ThirdPartyLocation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod); }; } // namespace QMatrixClient diff --git a/lib/application-service/definitions/protocol.cpp b/lib/application-service/definitions/protocol.cpp index 04bb7dfc..2a62b15d 100644 --- a/lib/application-service/definitions/protocol.cpp +++ b/lib/application-service/definitions/protocol.cpp @@ -6,75 +6,55 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const FieldType& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const FieldType& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("regexp"), pod.regexp); addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); - return jo; } -FieldType FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, FieldType& result) { - FieldType result; - result.regexp = - fromJson(jo.value("regexp"_ls)); - result.placeholder = - fromJson(jo.value("placeholder"_ls)); - - return result; + fromJson(jo.value("regexp"_ls), result.regexp); + fromJson(jo.value("placeholder"_ls), result.placeholder); } -QJsonObject QMatrixClient::toJson(const ProtocolInstance& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const ProtocolInstance& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("desc"), pod.desc); addParam(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("fields"), pod.fields); addParam<>(jo, QStringLiteral("network_id"), pod.networkId); - return jo; } -ProtocolInstance FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, ProtocolInstance& result) { - ProtocolInstance result; - result.desc = - fromJson(jo.value("desc"_ls)); - result.icon = - fromJson(jo.value("icon"_ls)); - result.fields = - fromJson(jo.value("fields"_ls)); - result.networkId = - fromJson(jo.value("network_id"_ls)); - - return result; + fromJson(jo.value("desc"_ls), result.desc); + fromJson(jo.value("icon"_ls), result.icon); + fromJson(jo.value("fields"_ls), result.fields); + fromJson(jo.value("network_id"_ls), result.networkId); } -QJsonObject QMatrixClient::toJson(const ThirdPartyProtocol& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const ThirdPartyProtocol& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); addParam<>(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); addParam<>(jo, QStringLiteral("instances"), pod.instances); - return jo; } -ThirdPartyProtocol FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, ThirdPartyProtocol& result) { - ThirdPartyProtocol result; - result.userFields = - fromJson(jo.value("user_fields"_ls)); - result.locationFields = - fromJson(jo.value("location_fields"_ls)); - result.icon = - fromJson(jo.value("icon"_ls)); - result.fieldTypes = - fromJson>(jo.value("field_types"_ls)); - result.instances = - fromJson>(jo.value("instances"_ls)); - - return result; + fromJson(jo.value("user_fields"_ls), result.userFields); + fromJson(jo.value("location_fields"_ls), result.locationFields); + fromJson(jo.value("icon"_ls), result.icon); + fromJson(jo.value("field_types"_ls), result.fieldTypes); + fromJson(jo.value("instances"_ls), result.instances); } diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 2aca7d66..0a1f9a21 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -25,12 +25,10 @@ namespace QMatrixClient /// An placeholder serving as a valid example of the field value. QString placeholder; }; - - QJsonObject toJson(const FieldType& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - FieldType operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const FieldType& pod); + static void fillFrom(const QJsonObject& jo, FieldType& pod); }; struct ProtocolInstance @@ -45,12 +43,10 @@ namespace QMatrixClient /// A unique identifier across all instances. QString networkId; }; - - QJsonObject toJson(const ProtocolInstance& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - ProtocolInstance operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod); + static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod); }; struct ThirdPartyProtocol @@ -78,12 +74,10 @@ namespace QMatrixClient /// same application service. QVector instances; }; - - QJsonObject toJson(const ThirdPartyProtocol& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - ThirdPartyProtocol operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod); }; } // namespace QMatrixClient diff --git a/lib/application-service/definitions/user.cpp b/lib/application-service/definitions/user.cpp index ca334236..8ba92321 100644 --- a/lib/application-service/definitions/user.cpp +++ b/lib/application-service/definitions/user.cpp @@ -6,25 +6,19 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const ThirdPartyUser& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const ThirdPartyUser& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("userid"), pod.userid); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; } -ThirdPartyUser FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, ThirdPartyUser& result) { - ThirdPartyUser result; - result.userid = - fromJson(jo.value("userid"_ls)); - result.protocol = - fromJson(jo.value("protocol"_ls)); - result.fields = - fromJson(jo.value("fields"_ls)); - - return result; + fromJson(jo.value("userid"_ls), result.userid); + fromJson(jo.value("protocol"_ls), result.protocol); + fromJson(jo.value("fields"_ls), result.fields); } diff --git a/lib/application-service/definitions/user.h b/lib/application-service/definitions/user.h index 79ca7789..062d2cac 100644 --- a/lib/application-service/definitions/user.h +++ b/lib/application-service/definitions/user.h @@ -21,12 +21,10 @@ namespace QMatrixClient /// Information used to identify this third party location. QJsonObject fields; }; - - QJsonObject toJson(const ThirdPartyUser& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - ThirdPartyUser operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod); }; } // namespace QMatrixClient diff --git a/lib/converters.cpp b/lib/converters.cpp index 41a9a65e..88f5267e 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -22,38 +22,34 @@ using namespace QMatrixClient; -QJsonValue QMatrixClient::variantToJson(const QVariant& v) +QJsonValue JsonConverter::dump(const QVariant& v) { return QJsonValue::fromVariant(v); } -QJsonObject QMatrixClient::toJson(const QVariantMap& map) +QVariant JsonConverter::load(const QJsonValue& jv) { - return QJsonObject::fromVariantMap(map); + return jv.toVariant(); } -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QJsonObject QMatrixClient::toJson(const QVariantHash& hMap) +QJsonObject JsonConverter::dump(const variant_map_t& map) { - return QJsonObject::fromVariantHash(hMap); -} + return +#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) + QJsonObject::fromVariantHash +#else + QJsonObject::fromVariantMap #endif - -QVariant FromJson::operator()(const QJsonValue& jv) const -{ - return jv.toVariant(); + (map); } -QMap -FromJson>::operator()(const QJsonValue& jv) const +variant_map_t JsonConverter::load(const QJsonValue& jv) { - return jv.toObject().toVariantMap(); -} - + return jv.toObject(). #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QHash -FromJson>::operator()(const QJsonValue& jv) const -{ - return jv.toObject().toVariantHash(); -} + toVariantHash +#else + toVariantMap #endif + (); +} diff --git a/lib/converters.h b/lib/converters.h index 53855a1f..6227902d 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -57,238 +57,221 @@ class QVariant; namespace QMatrixClient { - // This catches anything implicitly convertible to QJsonValue/Object/Array - 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 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; } - - inline QJsonArray toJson(const QStringList& strings) - { - return QJsonArray::fromStringList(strings); - } - - inline QString toJson(const QByteArray& bytes) + template + struct JsonObjectConverter { - return bytes.constData(); - } + static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; } + static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; } + }; - // QVariant is outrageously omnivorous - it consumes whatever is not - // exactly matching the signature of other toJson overloads. The trick - // below disables implicit conversion to QVariant through its numerous - // non-explicit constructors. - QJsonValue variantToJson(const QVariant& v); template - inline auto toJson(T&& /* const QVariant& or QVariant&& */ var) - -> std::enable_if_t, QVariant>::value, - QJsonValue> + struct JsonConverter { - return variantToJson(var); - } - QJsonObject toJson(const QMap& map); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - QJsonObject toJson(const QHash& hMap); -#endif + static QJsonObject dump(const T& pod) + { + QJsonObject jo; + JsonObjectConverter::dumpTo(jo, pod); + return jo; + } + static T doLoad(const QJsonObject& jo) + { + T pod; + JsonObjectConverter::fillFrom(jo, pod); + return pod; + } + static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } + static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } + }; template - inline QJsonArray toJson(const std::vector& vals) + inline auto toJson(const T& pod) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + return JsonConverter::dump(pod); } template - inline QJsonArray toJson(const QVector& vals) + inline auto fillJson(QJsonObject& json, const T& data) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + JsonObjectConverter::dumpTo(json, data); } template - inline QJsonObject toJson(const QSet& set) + inline auto fromJson(const QJsonValue& jv) { - QJsonObject json; - for (auto e: set) - json.insert(toJson(e), QJsonObject{}); - return json; + return JsonConverter::load(jv); } template - inline QJsonObject toJson(const QHash& hashMap) + inline T fromJson(const QJsonDocument& jd) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + return JsonConverter::load(jd); } template - inline QJsonObject toJson(const std::unordered_map& hashMap) + inline void fromJson(const QJsonValue& jv, T& pod) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + pod = fromJson(jv); } template - struct FromJsonObject + inline void fromJson(const QJsonDocument& jd, T& pod) { - T operator()(const QJsonObject& jo) const { return T(jo); } - }; + pod = fromJson(jd); + } + // Unfolds Omittable<> template - struct FromJson + inline void fromJson(const QJsonValue& jv, Omittable& pod) { - T operator()(const QJsonValue& jv) const - { - return FromJsonObject()(jv.toObject()); - } - T operator()(const QJsonDocument& jd) const - { - return FromJsonObject()(jd.object()); - } - }; + pod = fromJson(jv); + } template - inline auto fromJson(const QJsonValue& jv) + inline void fillFromJson(const QJsonValue& jv, T& pod) { - return FromJson()(jv); + JsonObjectConverter::fillFrom(jv.toObject(), pod); } + // JsonConverter<> specialisations + template - inline auto fromJson(const QJsonDocument& jd) + struct TrivialJsonDumper { - return FromJson()(jd); - } + // Works for: QJsonValue (and all things it can consume), + // QJsonObject, QJsonArray + static auto dump(const T& val) { return val; } + }; - template <> struct FromJson + template <> struct JsonConverter : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const { return jv.toBool(); } + static auto load(const QJsonValue& jv) { return jv.toBool(); } }; - template <> struct FromJson + template <> struct JsonConverter : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const { return jv.toInt(); } + static auto load(const QJsonValue& jv) { return jv.toInt(); } }; - template <> struct FromJson + template <> struct JsonConverter + : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const { return jv.toDouble(); } + static auto load(const QJsonValue& jv) { return jv.toDouble(); } }; - template <> struct FromJson + template <> struct JsonConverter : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const { return float(jv.toDouble()); } + static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } }; - template <> struct FromJson + template <> struct JsonConverter + : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } + static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } }; - template <> struct FromJson + template <> struct JsonConverter + : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const { return jv.toString(); } + static auto load(const QJsonValue& jv) { return jv.toString(); } }; - template <> struct FromJson + template <> struct JsonConverter { - auto operator()(const QJsonValue& jv) const + static auto dump(const QDateTime& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { - return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); + return QDateTime::fromMSecsSinceEpoch( + fromJson(jv), Qt::UTC); } }; - template <> struct FromJson + template <> struct JsonConverter { - auto operator()(const QJsonValue& jv) const + static auto dump(const QDate& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { return fromJson(jv).date(); } }; - template <> struct FromJson + template <> struct JsonConverter + : public TrivialJsonDumper { - auto operator()(const QJsonValue& jv) const - { - return jv.toArray(); - } + static auto load(const QJsonValue& jv) { return jv.toArray(); } }; - template <> struct FromJson + template <> struct JsonConverter { - auto operator()(const QJsonValue& jv) const + static QString dump(const QByteArray& ba) { return ba.constData(); } + static auto load(const QJsonValue& jv) { return fromJson(jv).toLatin1(); } }; - template <> struct FromJson + template <> struct JsonConverter { - QVariant operator()(const QJsonValue& jv) const; + static QJsonValue dump(const QVariant& v); + static QVariant load(const QJsonValue& jv); }; - template - struct ArrayFromJson + template + struct JsonArrayConverter { - auto operator()(const QJsonArray& ja) const + static void dumpTo(QJsonArray& ar, const VectorT& vals) { - using size_type = typename VectorT::size_type; - VectorT vect; vect.resize(size_type(ja.size())); - std::transform(ja.begin(), ja.end(), - vect.begin(), FromJson()); - return vect; + for (const auto& v: vals) + ar.push_back(toJson(v)); } - auto operator()(const QJsonValue& jv) const + static auto dump(const VectorT& vals) { - return operator()(jv.toArray()); + QJsonArray ja; + dumpTo(ja, vals); + return ja; } - auto operator()(const QJsonDocument& jd) const + static auto load(const QJsonArray& ja) { - return operator()(jd.array()); + VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); + for (const auto& i: ja) + vect.push_back(fromJson(i)); + return vect; } + static auto load(const QJsonValue& jv) { return load(jv.toArray()); } + static auto load(const QJsonDocument& jd) { return load(jd.array()); } }; - template - struct FromJson> : ArrayFromJson> + template struct JsonConverter> + : public JsonArrayConverter> { }; - template - struct FromJson> : ArrayFromJson> + template struct JsonConverter> + : public JsonArrayConverter> + { }; + + template struct JsonConverter> + : public JsonArrayConverter> { }; - template struct FromJson> + template <> struct JsonConverter + : public JsonConverter> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QStringList& sl) { - const auto jsonArray = jv.toArray(); - QList sl; sl.reserve(jsonArray.size()); - std::transform(jsonArray.begin(), jsonArray.end(), - std::back_inserter(sl), FromJson()); - return sl; + return QJsonArray::fromStringList(sl); } }; - template <> struct FromJson : FromJson> { }; - - template <> struct FromJson> + template <> struct JsonObjectConverter> { - QMap operator()(const QJsonValue& jv) const; - }; - - template struct FromJson> - { - auto operator()(const QJsonValue& jv) const + static void dumpTo(QJsonObject& json, const QSet& s) + { + for (const auto& e: s) + json.insert(toJson(e), QJsonObject{}); + } + static auto fillFrom(const QJsonObject& json, QSet& s) { - const auto json = jv.toObject(); - QSet s; s.reserve(json.size()); + s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); return s; @@ -298,39 +281,44 @@ namespace QMatrixClient template struct HashMapFromJson { - auto operator()(const QJsonObject& jo) const + static void dumpTo(QJsonObject& json, const HashMapT& hashMap) + { + for (auto it = hashMap.begin(); it != hashMap.end(); ++it) + json.insert(it.key(), toJson(it.value())); + } + static void fillFrom(const QJsonObject& jo, HashMapT& h) { - HashMapT h; h.reserve(jo.size()); + h.reserve(jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson(it.value()); - return h; - } - auto operator()(const QJsonValue& jv) const - { - return operator()(jv.toObject()); - } - auto operator()(const QJsonDocument& jd) const - { - return operator()(jd.object()); } }; template - struct FromJson> - : HashMapFromJson> + struct JsonObjectConverter> + : public HashMapFromJson> { }; template - struct FromJson> : HashMapFromJson> + struct JsonObjectConverter> + : public HashMapFromJson> { }; + // We could use std::conditional<> below but QT_VERSION* macros in C++ code + // cause (kinda valid but useless and noisy) compiler warnings about + // bitwise operations on signed integers; so use the preprocessor for now. + using variant_map_t = #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - template <> struct FromJson> + QVariantHash; +#else + QVariantMap; +#endif + template <> struct JsonConverter { - QHash operator()(const QJsonValue& jv) const; + static QJsonObject dump(const variant_map_t& vh); + static QVariantHash load(const QJsonValue& jv); }; -#endif // Conditional insertion into a QJsonObject diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 6066d4d9..ce06a56d 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -16,43 +16,29 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetWhoIsJob::ConnectionInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::ConnectionInfo& result) { - GetWhoIsJob::ConnectionInfo result; - result.ip = - fromJson(jo.value("ip"_ls)); - result.lastSeen = - fromJson(jo.value("last_seen"_ls)); - result.userAgent = - fromJson(jo.value("user_agent"_ls)); - - return result; + fromJson(jo.value("ip"_ls), result.ip); + fromJson(jo.value("last_seen"_ls), result.lastSeen); + fromJson(jo.value("user_agent"_ls), result.userAgent); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetWhoIsJob::SessionInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) { - GetWhoIsJob::SessionInfo result; - result.connections = - fromJson>(jo.value("connections"_ls)); - - return result; + fromJson(jo.value("connections"_ls), result.connections); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetWhoIsJob::DeviceInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) { - GetWhoIsJob::DeviceInfo result; - result.sessions = - fromJson>(jo.value("sessions"_ls)); - - return result; + fromJson(jo.value("sessions"_ls), result.sessions); } }; } // namespace QMatrixClient @@ -94,8 +80,8 @@ const QHash& GetWhoIsJob::devices() const BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->userId = fromJson(json.value("user_id"_ls)); - d->devices = fromJson>(json.value("devices"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("devices"_ls), d->devices); return Success; } diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index f62002a6..9b021e17 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -16,21 +16,14 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetAccount3PIDsJob::ThirdPartyIdentifier operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetAccount3PIDsJob::ThirdPartyIdentifier& result) { - GetAccount3PIDsJob::ThirdPartyIdentifier result; - result.medium = - fromJson(jo.value("medium"_ls)); - result.address = - fromJson(jo.value("address"_ls)); - result.validatedAt = - fromJson(jo.value("validated_at"_ls)); - result.addedAt = - fromJson(jo.value("added_at"_ls)); - - return result; + fromJson(jo.value("medium"_ls), result.medium); + fromJson(jo.value("address"_ls), result.address); + fromJson(jo.value("validated_at"_ls), result.validatedAt); + fromJson(jo.value("added_at"_ls), result.addedAt); } }; } // namespace QMatrixClient @@ -66,7 +59,7 @@ const QVector& GetAccount3PIDsJob::thr BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->threepids = fromJson>(json.value("threepids"_ls)); + fromJson(json.value("threepids"_ls), d->threepids); return Success; } @@ -74,14 +67,15 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const Post3PIDsJob::ThreePidCredentials& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; - } + static void dumpTo(QJsonObject& jo, const Post3PIDsJob::ThreePidCredentials& pod) + { + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("sid"), pod.sid); + } + }; } // namespace QMatrixClient static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); @@ -139,7 +133,7 @@ const Sid& RequestTokenTo3PIDEmailJob::data() const BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -175,7 +169,7 @@ const Sid& RequestTokenTo3PIDMSISDNJob::data() const BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 9b590e42..22223985 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -52,7 +52,7 @@ BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) if (!json.contains("content_uri"_ls)) return { JsonParseError, "The key 'content_uri' not found in the response" }; - d->contentUri = fromJson(json.value("content_uri"_ls)); + fromJson(json.value("content_uri"_ls), d->contentUri); return Success; } @@ -276,8 +276,8 @@ const QString& GetUrlPreviewJob::ogImage() const BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->matrixImageSize = fromJson(json.value("matrix:image:size"_ls)); - d->ogImage = fromJson(json.value("og:image"_ls)); + fromJson(json.value("matrix:image:size"_ls), d->matrixImageSize); + fromJson(json.value("og:image"_ls), d->ogImage); return Success; } @@ -312,7 +312,7 @@ Omittable GetConfigJob::uploadSize() const BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->uploadSize = fromJson(json.value("m.upload.size"_ls)); + fromJson(json.value("m.upload.size"_ls), d->uploadSize); return Success; } diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 36f83727..c44b73ac 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -16,23 +16,25 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const CreateRoomJob::Invite3pid& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("medium"), pod.medium); - addParam<>(jo, QStringLiteral("address"), pod.address); - return jo; - } + static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) + { + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("medium"), pod.medium); + addParam<>(jo, QStringLiteral("address"), pod.address); + } + }; - QJsonObject toJson(const CreateRoomJob::StateEvent& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam(jo, QStringLiteral("state_key"), pod.stateKey); - addParam<>(jo, QStringLiteral("content"), pod.content); - return jo; - } + static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) + { + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam(jo, QStringLiteral("state_key"), pod.stateKey); + addParam<>(jo, QStringLiteral("content"), pod.content); + } + }; } // namespace QMatrixClient class CreateRoomJob::Private @@ -77,7 +79,7 @@ BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } diff --git a/lib/csapi/definitions/auth_data.cpp b/lib/csapi/definitions/auth_data.cpp index f8639432..006b8c7e 100644 --- a/lib/csapi/definitions/auth_data.cpp +++ b/lib/csapi/definitions/auth_data.cpp @@ -6,23 +6,20 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const AuthenticationData& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const AuthenticationData& pod) { - QJsonObject jo = toJson(pod.authInfo); + fillJson(jo, pod.authInfo); addParam<>(jo, QStringLiteral("type"), pod.type); addParam(jo, QStringLiteral("session"), pod.session); - return jo; } -AuthenticationData FromJsonObject::operator()(QJsonObject jo) const +void JsonObjectConverter::fillFrom( + QJsonObject jo, AuthenticationData& result) { - AuthenticationData result; - result.type = - fromJson(jo.take("type"_ls)); - result.session = - fromJson(jo.take("session"_ls)); + fromJson(jo.take("type"_ls), result.type); + fromJson(jo.take("session"_ls), result.session); - result.authInfo = fromJson>(jo); - return result; + fromJson(jo, result.authInfo); } diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h index 661d3e5f..26eb205c 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -23,12 +23,10 @@ namespace QMatrixClient /// Keys dependent on the login type QHash authInfo; }; - - QJsonObject toJson(const AuthenticationData& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - AuthenticationData operator()(QJsonObject jo) const; + static void dumpTo(QJsonObject& jo, const AuthenticationData& pod); + static void fillFrom(QJsonObject jo, AuthenticationData& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/client_device.cpp b/lib/csapi/definitions/client_device.cpp index 4a192f85..752b806a 100644 --- a/lib/csapi/definitions/client_device.cpp +++ b/lib/csapi/definitions/client_device.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const Device& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const Device& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam(jo, QStringLiteral("display_name"), pod.displayName); addParam(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); addParam(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); - return jo; } -Device FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, Device& result) { - Device result; - result.deviceId = - fromJson(jo.value("device_id"_ls)); - result.displayName = - fromJson(jo.value("display_name"_ls)); - result.lastSeenIp = - fromJson(jo.value("last_seen_ip"_ls)); - result.lastSeenTs = - fromJson(jo.value("last_seen_ts"_ls)); - - return result; + fromJson(jo.value("device_id"_ls), result.deviceId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("last_seen_ip"_ls), result.lastSeenIp); + fromJson(jo.value("last_seen_ts"_ls), result.lastSeenTs); } diff --git a/lib/csapi/definitions/client_device.h b/lib/csapi/definitions/client_device.h index 9f10888a..a6224f71 100644 --- a/lib/csapi/definitions/client_device.h +++ b/lib/csapi/definitions/client_device.h @@ -28,12 +28,10 @@ namespace QMatrixClient /// reasons). Omittable lastSeenTs; }; - - QJsonObject toJson(const Device& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - Device operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Device& pod); + static void fillFrom(const QJsonObject& jo, Device& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/device_keys.cpp b/lib/csapi/definitions/device_keys.cpp index a0e0ca42..1e79499f 100644 --- a/lib/csapi/definitions/device_keys.cpp +++ b/lib/csapi/definitions/device_keys.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const DeviceKeys& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const DeviceKeys& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("user_id"), pod.userId); addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); addParam<>(jo, QStringLiteral("keys"), pod.keys); addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; } -DeviceKeys FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, DeviceKeys& result) { - DeviceKeys result; - result.userId = - fromJson(jo.value("user_id"_ls)); - result.deviceId = - fromJson(jo.value("device_id"_ls)); - result.algorithms = - fromJson(jo.value("algorithms"_ls)); - result.keys = - fromJson>(jo.value("keys"_ls)); - result.signatures = - fromJson>>(jo.value("signatures"_ls)); - - return result; + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("device_id"_ls), result.deviceId); + fromJson(jo.value("algorithms"_ls), result.algorithms); + fromJson(jo.value("keys"_ls), result.keys); + fromJson(jo.value("signatures"_ls), result.signatures); } diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 6023e7e8..8ebe1125 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -34,12 +34,10 @@ namespace QMatrixClient /// JSON`_. QHash> signatures; }; - - QJsonObject toJson(const DeviceKeys& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - DeviceKeys operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const DeviceKeys& pod); + static void fillFrom(const QJsonObject& jo, DeviceKeys& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/event_filter.cpp b/lib/csapi/definitions/event_filter.cpp index cc444db0..b20d7807 100644 --- a/lib/csapi/definitions/event_filter.cpp +++ b/lib/csapi/definitions/event_filter.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const EventFilter& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const EventFilter& pod) { - QJsonObject jo; addParam(jo, QStringLiteral("limit"), pod.limit); addParam(jo, QStringLiteral("not_senders"), pod.notSenders); addParam(jo, QStringLiteral("not_types"), pod.notTypes); addParam(jo, QStringLiteral("senders"), pod.senders); addParam(jo, QStringLiteral("types"), pod.types); - return jo; } -EventFilter FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, EventFilter& result) { - EventFilter result; - result.limit = - fromJson(jo.value("limit"_ls)); - result.notSenders = - fromJson(jo.value("not_senders"_ls)); - result.notTypes = - fromJson(jo.value("not_types"_ls)); - result.senders = - fromJson(jo.value("senders"_ls)); - result.types = - fromJson(jo.value("types"_ls)); - - return result; + fromJson(jo.value("limit"_ls), result.limit); + fromJson(jo.value("not_senders"_ls), result.notSenders); + fromJson(jo.value("not_types"_ls), result.notTypes); + fromJson(jo.value("senders"_ls), result.senders); + fromJson(jo.value("types"_ls), result.types); } diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 5c6a5b27..6de1fe79 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -25,12 +25,10 @@ namespace QMatrixClient /// A list of event types to include. If this list is absent then all event types are included. A ``'*'`` can be used as a wildcard to match any sequence of characters. QStringList types; }; - - QJsonObject toJson(const EventFilter& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - EventFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const EventFilter& pod); + static void fillFrom(const QJsonObject& jo, EventFilter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/public_rooms_response.cpp b/lib/csapi/definitions/public_rooms_response.cpp index 2f52501d..0d26662c 100644 --- a/lib/csapi/definitions/public_rooms_response.cpp +++ b/lib/csapi/definitions/public_rooms_response.cpp @@ -6,9 +6,9 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const PublicRoomsChunk& pod) { - QJsonObject jo; addParam(jo, QStringLiteral("aliases"), pod.aliases); addParam(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); addParam(jo, QStringLiteral("name"), pod.name); @@ -18,56 +18,37 @@ QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); - return jo; } -PublicRoomsChunk FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, PublicRoomsChunk& result) { - PublicRoomsChunk result; - result.aliases = - fromJson(jo.value("aliases"_ls)); - result.canonicalAlias = - fromJson(jo.value("canonical_alias"_ls)); - result.name = - fromJson(jo.value("name"_ls)); - result.numJoinedMembers = - fromJson(jo.value("num_joined_members"_ls)); - result.roomId = - fromJson(jo.value("room_id"_ls)); - result.topic = - fromJson(jo.value("topic"_ls)); - result.worldReadable = - fromJson(jo.value("world_readable"_ls)); - result.guestCanJoin = - fromJson(jo.value("guest_can_join"_ls)); - result.avatarUrl = - fromJson(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("aliases"_ls), result.aliases); + fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); + fromJson(jo.value("name"_ls), result.name); + fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("topic"_ls), result.topic); + fromJson(jo.value("world_readable"_ls), result.worldReadable); + fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } -QJsonObject QMatrixClient::toJson(const PublicRoomsResponse& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const PublicRoomsResponse& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("chunk"), pod.chunk); addParam(jo, QStringLiteral("next_batch"), pod.nextBatch); addParam(jo, QStringLiteral("prev_batch"), pod.prevBatch); addParam(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate); - return jo; } -PublicRoomsResponse FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, PublicRoomsResponse& result) { - PublicRoomsResponse result; - result.chunk = - fromJson>(jo.value("chunk"_ls)); - result.nextBatch = - fromJson(jo.value("next_batch"_ls)); - result.prevBatch = - fromJson(jo.value("prev_batch"_ls)); - result.totalRoomCountEstimate = - fromJson(jo.value("total_room_count_estimate"_ls)); - - return result; + fromJson(jo.value("chunk"_ls), result.chunk); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("prev_batch"_ls), result.prevBatch); + fromJson(jo.value("total_room_count_estimate"_ls), result.totalRoomCountEstimate); } diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 88c805ba..4c54ac25 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -36,12 +36,10 @@ namespace QMatrixClient /// The URL for the room's avatar, if one is set. QString avatarUrl; }; - - QJsonObject toJson(const PublicRoomsChunk& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - PublicRoomsChunk operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod); + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod); }; /// A list of the rooms on the server. @@ -61,12 +59,10 @@ namespace QMatrixClient /// server has an estimate. Omittable totalRoomCountEstimate; }; - - QJsonObject toJson(const PublicRoomsResponse& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - PublicRoomsResponse operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod); + static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_condition.cpp b/lib/csapi/definitions/push_condition.cpp index 045094bc..ace02755 100644 --- a/lib/csapi/definitions/push_condition.cpp +++ b/lib/csapi/definitions/push_condition.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushCondition& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const PushCondition& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("kind"), pod.kind); addParam(jo, QStringLiteral("key"), pod.key); addParam(jo, QStringLiteral("pattern"), pod.pattern); addParam(jo, QStringLiteral("is"), pod.is); - return jo; } -PushCondition FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, PushCondition& result) { - PushCondition result; - result.kind = - fromJson(jo.value("kind"_ls)); - result.key = - fromJson(jo.value("key"_ls)); - result.pattern = - fromJson(jo.value("pattern"_ls)); - result.is = - fromJson(jo.value("is"_ls)); - - return result; + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("key"_ls), result.key); + fromJson(jo.value("pattern"_ls), result.pattern); + fromJson(jo.value("is"_ls), result.is); } diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index defcebb3..e45526d2 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -28,12 +28,10 @@ namespace QMatrixClient /// so forth. If no prefix is present, this parameter defaults to ==. QString is; }; - - QJsonObject toJson(const PushCondition& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - PushCondition operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushCondition& pod); + static void fillFrom(const QJsonObject& jo, PushCondition& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_rule.cpp b/lib/csapi/definitions/push_rule.cpp index baddd187..abbb04b5 100644 --- a/lib/csapi/definitions/push_rule.cpp +++ b/lib/csapi/definitions/push_rule.cpp @@ -6,34 +6,25 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushRule& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const PushRule& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("actions"), pod.actions); addParam<>(jo, QStringLiteral("default"), pod.isDefault); addParam<>(jo, QStringLiteral("enabled"), pod.enabled); addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); addParam(jo, QStringLiteral("conditions"), pod.conditions); addParam(jo, QStringLiteral("pattern"), pod.pattern); - return jo; } -PushRule FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, PushRule& result) { - PushRule result; - result.actions = - fromJson>(jo.value("actions"_ls)); - result.isDefault = - fromJson(jo.value("default"_ls)); - result.enabled = - fromJson(jo.value("enabled"_ls)); - result.ruleId = - fromJson(jo.value("rule_id"_ls)); - result.conditions = - fromJson>(jo.value("conditions"_ls)); - result.pattern = - fromJson(jo.value("pattern"_ls)); - - return result; + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("default"_ls), result.isDefault); + fromJson(jo.value("enabled"_ls), result.enabled); + fromJson(jo.value("rule_id"_ls), result.ruleId); + fromJson(jo.value("conditions"_ls), result.conditions); + fromJson(jo.value("pattern"_ls), result.pattern); } diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 5f52876d..05328b8b 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -34,12 +34,10 @@ namespace QMatrixClient /// rules. QString pattern; }; - - QJsonObject toJson(const PushRule& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - PushRule operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushRule& pod); + static void fillFrom(const QJsonObject& jo, PushRule& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_ruleset.cpp b/lib/csapi/definitions/push_ruleset.cpp index 14b7a4b6..f1bad882 100644 --- a/lib/csapi/definitions/push_ruleset.cpp +++ b/lib/csapi/definitions/push_ruleset.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushRuleset& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const PushRuleset& pod) { - QJsonObject jo; addParam(jo, QStringLiteral("content"), pod.content); addParam(jo, QStringLiteral("override"), pod.override); addParam(jo, QStringLiteral("room"), pod.room); addParam(jo, QStringLiteral("sender"), pod.sender); addParam(jo, QStringLiteral("underride"), pod.underride); - return jo; } -PushRuleset FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, PushRuleset& result) { - PushRuleset result; - result.content = - fromJson>(jo.value("content"_ls)); - result.override = - fromJson>(jo.value("override"_ls)); - result.room = - fromJson>(jo.value("room"_ls)); - result.sender = - fromJson>(jo.value("sender"_ls)); - result.underride = - fromJson>(jo.value("underride"_ls)); - - return result; + fromJson(jo.value("content"_ls), result.content); + fromJson(jo.value("override"_ls), result.override); + fromJson(jo.value("room"_ls), result.room); + fromJson(jo.value("sender"_ls), result.sender); + fromJson(jo.value("underride"_ls), result.underride); } diff --git a/lib/csapi/definitions/push_ruleset.h b/lib/csapi/definitions/push_ruleset.h index a274b72a..f2d937c0 100644 --- a/lib/csapi/definitions/push_ruleset.h +++ b/lib/csapi/definitions/push_ruleset.h @@ -22,12 +22,10 @@ namespace QMatrixClient QVector sender; QVector underride; }; - - QJsonObject toJson(const PushRuleset& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - PushRuleset operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushRuleset& pod); + static void fillFrom(const QJsonObject& jo, PushRuleset& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp index 8cd2ded7..df92e684 100644 --- a/lib/csapi/definitions/room_event_filter.cpp +++ b/lib/csapi/definitions/room_event_filter.cpp @@ -6,31 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const RoomEventFilter& pod) { - QJsonObject jo; + fillJson(jo, pod); addParam(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam(jo, QStringLiteral("rooms"), pod.rooms); addParam(jo, QStringLiteral("contains_url"), pod.containsUrl); - addParam(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); - addParam(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); - return jo; } -RoomEventFilter FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, RoomEventFilter& result) { - RoomEventFilter result; - result.notRooms = - fromJson(jo.value("not_rooms"_ls)); - result.rooms = - fromJson(jo.value("rooms"_ls)); - result.containsUrl = - fromJson(jo.value("contains_url"_ls)); - result.lazyLoadMembers = - fromJson(jo.value("lazy_load_members"_ls)); - result.includeRedundantMembers = - fromJson(jo.value("include_redundant_members"_ls)); - - return result; + fillFromJson(jo, result); + fromJson(jo.value("not_rooms"_ls), result.notRooms); + fromJson(jo.value("rooms"_ls), result.rooms); + fromJson(jo.value("contains_url"_ls), result.containsUrl); } diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 87f01189..3908b8ec 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -21,17 +21,11 @@ namespace QMatrixClient QStringList rooms; /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``. bool containsUrl; - /// If ``true``, the only ``m.room.member`` events returned in the ``state`` section of the ``/sync`` response are those which are definitely necessary for a client to display the ``sender`` of the timeline events in that response. If ``false``, ``m.room.member`` events are not filtered. By default, servers should suppress duplicate redundant lazy-loaded ``m.room.member`` events from being sent to a given client across multiple calls to ``/sync``, given that most clients cache membership events (see include_redundant_members to change this behaviour). - bool lazyLoadMembers; - /// If ``true``, the ``state`` section of the ``/sync`` response will always contain the ``m.room.member`` events required to display the ``sender`` of the timeline events in that response, assuming ``lazy_load_members`` is enabled. This means that redundant duplicate member events may be returned across multiple calls to ``/sync``. This is useful for naive clients who never track membership data. If ``false``, duplicate ``m.room.member`` events may be suppressed by the server across multiple calls to ``/sync``. If ``lazy_load_members`` is ``false`` this field is ignored. - bool includeRedundantMembers; }; - - QJsonObject toJson(const RoomEventFilter& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - RoomEventFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod); + static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/sync_filter.cpp b/lib/csapi/definitions/sync_filter.cpp index bd87804c..32752d1f 100644 --- a/lib/csapi/definitions/sync_filter.cpp +++ b/lib/csapi/definitions/sync_filter.cpp @@ -6,9 +6,25 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RoomFilter& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const StateFilter& pod) +{ + fillJson(jo, pod); + addParam(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); + addParam(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); +} + +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, StateFilter& result) +{ + fillFromJson(jo, result); + fromJson(jo.value("lazy_load_members"_ls), result.lazyLoadMembers); + fromJson(jo.value("include_redundant_members"_ls), result.includeRedundantMembers); +} + +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const RoomFilter& pod) { - QJsonObject jo; addParam(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam(jo, QStringLiteral("rooms"), pod.rooms); addParam(jo, QStringLiteral("ephemeral"), pod.ephemeral); @@ -16,55 +32,37 @@ QJsonObject QMatrixClient::toJson(const RoomFilter& pod) addParam(jo, QStringLiteral("state"), pod.state); addParam(jo, QStringLiteral("timeline"), pod.timeline); addParam(jo, QStringLiteral("account_data"), pod.accountData); - return jo; } -RoomFilter FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, RoomFilter& result) { - RoomFilter result; - result.notRooms = - fromJson(jo.value("not_rooms"_ls)); - result.rooms = - fromJson(jo.value("rooms"_ls)); - result.ephemeral = - fromJson(jo.value("ephemeral"_ls)); - result.includeLeave = - fromJson(jo.value("include_leave"_ls)); - result.state = - fromJson(jo.value("state"_ls)); - result.timeline = - fromJson(jo.value("timeline"_ls)); - result.accountData = - fromJson(jo.value("account_data"_ls)); - - return result; + fromJson(jo.value("not_rooms"_ls), result.notRooms); + fromJson(jo.value("rooms"_ls), result.rooms); + fromJson(jo.value("ephemeral"_ls), result.ephemeral); + fromJson(jo.value("include_leave"_ls), result.includeLeave); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("timeline"_ls), result.timeline); + fromJson(jo.value("account_data"_ls), result.accountData); } -QJsonObject QMatrixClient::toJson(const Filter& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const Filter& pod) { - QJsonObject jo; addParam(jo, QStringLiteral("event_fields"), pod.eventFields); addParam(jo, QStringLiteral("event_format"), pod.eventFormat); addParam(jo, QStringLiteral("presence"), pod.presence); addParam(jo, QStringLiteral("account_data"), pod.accountData); addParam(jo, QStringLiteral("room"), pod.room); - return jo; } -Filter FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, Filter& result) { - Filter result; - result.eventFields = - fromJson(jo.value("event_fields"_ls)); - result.eventFormat = - fromJson(jo.value("event_format"_ls)); - result.presence = - fromJson(jo.value("presence"_ls)); - result.accountData = - fromJson(jo.value("account_data"_ls)); - result.room = - fromJson(jo.value("room"_ls)); - - return result; + fromJson(jo.value("event_fields"_ls), result.eventFields); + fromJson(jo.value("event_format"_ls), result.eventFormat); + fromJson(jo.value("presence"_ls), result.presence); + fromJson(jo.value("account_data"_ls), result.accountData); + fromJson(jo.value("room"_ls), result.room); } diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index ca275a9a..ccc3061b 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -14,6 +14,37 @@ namespace QMatrixClient { // Data structures + /// The state events to include for rooms. + struct StateFilter : RoomEventFilter + { + /// If ``true``, the only ``m.room.member`` events returned in + /// the ``state`` section of the ``/sync`` response are those + /// which are definitely necessary for a client to display + /// the ``sender`` of the timeline events in that response. + /// If ``false``, ``m.room.member`` events are not filtered. + /// By default, servers should suppress duplicate redundant + /// lazy-loaded ``m.room.member`` events from being sent to a given + /// client across multiple calls to ``/sync``, given that most clients + /// cache membership events (see ``include_redundant_members`` + /// to change this behaviour). + bool lazyLoadMembers; + /// If ``true``, the ``state`` section of the ``/sync`` response will + /// always contain the ``m.room.member`` events required to display + /// the ``sender`` of the timeline events in that response, assuming + /// ``lazy_load_members`` is enabled. This means that redundant + /// duplicate member events may be returned across multiple calls to + /// ``/sync``. This is useful for naive clients who never track + /// membership data. If ``false``, duplicate ``m.room.member`` events + /// may be suppressed by the server across multiple calls to ``/sync``. + /// If ``lazy_load_members`` is ``false`` this field is ignored. + bool includeRedundantMembers; + }; + template <> struct JsonObjectConverter + { + static void dumpTo(QJsonObject& jo, const StateFilter& pod); + static void fillFrom(const QJsonObject& jo, StateFilter& pod); + }; + /// Filters to be applied to room data. struct RoomFilter { @@ -26,18 +57,16 @@ namespace QMatrixClient /// Include rooms that the user has left in the sync, default false bool includeLeave; /// The state events to include for rooms. - Omittable state; + Omittable state; /// The message and state update events to include for rooms. Omittable timeline; /// The per user account data to include for rooms. Omittable accountData; }; - - QJsonObject toJson(const RoomFilter& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - RoomFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RoomFilter& pod); + static void fillFrom(const QJsonObject& jo, RoomFilter& pod); }; struct Filter @@ -53,12 +82,10 @@ namespace QMatrixClient /// Filters to be applied to room data. Omittable room; }; - - QJsonObject toJson(const Filter& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - Filter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Filter& pod); + static void fillFrom(const QJsonObject& jo, Filter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/user_identifier.cpp b/lib/csapi/definitions/user_identifier.cpp index 80a6d450..05a27c1c 100644 --- a/lib/csapi/definitions/user_identifier.cpp +++ b/lib/csapi/definitions/user_identifier.cpp @@ -6,20 +6,18 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const UserIdentifier& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const UserIdentifier& pod) { - QJsonObject jo = toJson(pod.additionalProperties); + fillJson(jo, pod.additionalProperties); addParam<>(jo, QStringLiteral("type"), pod.type); - return jo; } -UserIdentifier FromJsonObject::operator()(QJsonObject jo) const +void JsonObjectConverter::fillFrom( + QJsonObject jo, UserIdentifier& result) { - UserIdentifier result; - result.type = - fromJson(jo.take("type"_ls)); + fromJson(jo.take("type"_ls), result.type); - result.additionalProperties = fromJson(jo); - return result; + fromJson(jo, result.additionalProperties); } diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index 42614436..cbb1550f 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -20,12 +20,10 @@ namespace QMatrixClient /// Identification information for a user QVariantHash additionalProperties; }; - - QJsonObject toJson(const UserIdentifier& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - UserIdentifier operator()(QJsonObject jo) const; + static void dumpTo(QJsonObject& jo, const UserIdentifier& pod); + static void fillFrom(QJsonObject jo, UserIdentifier& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/homeserver.cpp b/lib/csapi/definitions/wellknown/homeserver.cpp index f1482ee4..0783f11b 100644 --- a/lib/csapi/definitions/wellknown/homeserver.cpp +++ b/lib/csapi/definitions/wellknown/homeserver.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const HomeserverInformation& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const HomeserverInformation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; } -HomeserverInformation FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, HomeserverInformation& result) { - HomeserverInformation result; - result.baseUrl = - fromJson(jo.value("base_url"_ls)); - - return result; + fromJson(jo.value("base_url"_ls), result.baseUrl); } diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index 09d6ba63..f6761c30 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -17,12 +17,10 @@ namespace QMatrixClient /// The base URL for the homeserver for client-server connections. QString baseUrl; }; - - QJsonObject toJson(const HomeserverInformation& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - HomeserverInformation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod); + static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/identity_server.cpp b/lib/csapi/definitions/wellknown/identity_server.cpp index f9d7bc37..99f36641 100644 --- a/lib/csapi/definitions/wellknown/identity_server.cpp +++ b/lib/csapi/definitions/wellknown/identity_server.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const IdentityServerInformation& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const IdentityServerInformation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; } -IdentityServerInformation FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, IdentityServerInformation& result) { - IdentityServerInformation result; - result.baseUrl = - fromJson(jo.value("base_url"_ls)); - - return result; + fromJson(jo.value("base_url"_ls), result.baseUrl); } diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index cb8ffcee..67d8b08d 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -17,12 +17,10 @@ namespace QMatrixClient /// The base URL for the identity server for client-server connections. QString baseUrl; }; - - QJsonObject toJson(const IdentityServerInformation& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - IdentityServerInformation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod); + static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index 861e1994..9c31db5d 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -43,7 +43,7 @@ const QVector& GetDevicesJob::devices() const BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->devices = fromJson>(json.value("devices"_ls)); + fromJson(json.value("devices"_ls), d->devices); return Success; } @@ -77,7 +77,7 @@ const Device& GetDeviceJob::data() const BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 5353f3bc..4af86f7b 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -60,8 +60,8 @@ const QStringList& GetRoomIdByAliasJob::servers() const BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->roomId = fromJson(json.value("room_id"_ls)); - d->servers = fromJson(json.value("servers"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); + fromJson(json.value("servers"_ls), d->servers); return Success; } diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 806c1613..bb1f5301 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -82,12 +82,12 @@ StateEvents&& GetEventContextJob::state() BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson(json.value("start"_ls)); - d->end = fromJson(json.value("end"_ls)); - d->eventsBefore = fromJson(json.value("events_before"_ls)); - d->event = fromJson(json.value("event"_ls)); - d->eventsAfter = fromJson(json.value("events_after"_ls)); - d->state = fromJson(json.value("state"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("events_before"_ls), d->eventsBefore); + fromJson(json.value("event"_ls), d->event); + fromJson(json.value("events_after"_ls), d->eventsAfter); + fromJson(json.value("state"_ls), d->state); return Success; } diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 77dc9b92..982e60b5 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -41,7 +41,7 @@ BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data) if (!json.contains("filter_id"_ls)) return { JsonParseError, "The key 'filter_id' not found in the response" }; - d->filterId = fromJson(json.value("filter_id"_ls)); + fromJson(json.value("filter_id"_ls), d->filterId); return Success; } @@ -75,7 +75,7 @@ const Filter& GetFilterJob::data() const BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 71781154..00d930fa 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -16,15 +16,16 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const JoinRoomByIdJob::ThirdPartySigned& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } + static void dumpTo(QJsonObject& jo, const JoinRoomByIdJob::ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + }; } // namespace QMatrixClient class JoinRoomByIdJob::Private @@ -58,7 +59,7 @@ BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } @@ -66,22 +67,24 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const JoinRoomJob::Signed& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } - - QJsonObject toJson(const JoinRoomJob::ThirdPartySigned& pod) + static void dumpTo(QJsonObject& jo, const JoinRoomJob::Signed& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + }; + + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam<>(jo, QStringLiteral("signed"), pod.signedData); - return jo; - } + static void dumpTo(QJsonObject& jo, const JoinRoomJob::ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("signed"), pod.signedData); + } + }; } // namespace QMatrixClient class JoinRoomJob::Private @@ -123,7 +126,7 @@ BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index c7492411..6c16a8a3 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -44,7 +44,7 @@ BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data) if (!json.contains("one_time_key_counts"_ls)) return { JsonParseError, "The key 'one_time_key_counts' not found in the response" }; - d->oneTimeKeyCounts = fromJson>(json.value("one_time_key_counts"_ls)); + fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts); return Success; } @@ -52,27 +52,20 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - QueryKeysJob::UnsignedDeviceInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, QueryKeysJob::UnsignedDeviceInfo& result) { - QueryKeysJob::UnsignedDeviceInfo result; - result.deviceDisplayName = - fromJson(jo.value("device_display_name"_ls)); - - return result; + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - QueryKeysJob::DeviceInformation operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, QueryKeysJob::DeviceInformation& result) { - QueryKeysJob::DeviceInformation result; - result.unsignedData = - fromJson(jo.value("unsigned"_ls)); - - return result; + fillFromJson(jo, result); + fromJson(jo.value("unsigned"_ls), result.unsignedData); } }; } // namespace QMatrixClient @@ -113,8 +106,8 @@ const QHash>& QueryKeys BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->failures = fromJson>(json.value("failures"_ls)); - d->deviceKeys = fromJson>>(json.value("device_keys"_ls)); + fromJson(json.value("failures"_ls), d->failures); + fromJson(json.value("device_keys"_ls), d->deviceKeys); return Success; } @@ -153,8 +146,8 @@ const QHash>& ClaimKeysJob::oneTimeKeys() cons BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->failures = fromJson>(json.value("failures"_ls)); - d->oneTimeKeys = fromJson>>(json.value("one_time_keys"_ls)); + fromJson(json.value("failures"_ls), d->failures); + fromJson(json.value("one_time_keys"_ls), d->oneTimeKeys); return Success; } @@ -205,8 +198,8 @@ const QStringList& GetKeysChangesJob::left() const BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->changed = fromJson(json.value("changed"_ls)); - d->left = fromJson(json.value("left"_ls)); + fromJson(json.value("changed"_ls), d->changed); + fromJson(json.value("left"_ls), d->left); return Success; } diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index a745dba1..85a9cae4 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data) if (!json.contains("joined_rooms"_ls)) return { JsonParseError, "The key 'joined_rooms' not found in the response" }; - d->joinedRooms = fromJson(json.value("joined_rooms"_ls)); + fromJson(json.value("joined_rooms"_ls), d->joinedRooms); return Success; } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 2fdb2005..2a0cb0ff 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -43,7 +43,7 @@ const QString& GetRoomVisibilityOnDirectoryJob::visibility() const BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->visibility = fromJson(json.value("visibility"_ls)); + fromJson(json.value("visibility"_ls), d->visibility); return Success; } @@ -100,7 +100,7 @@ const PublicRoomsResponse& GetPublicRoomsJob::data() const BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -108,12 +108,13 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const QueryPublicRoomsJob::Filter& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); - return jo; - } + static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) + { + addParam(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); + } + }; } // namespace QMatrixClient class QueryPublicRoomsJob::Private @@ -155,7 +156,7 @@ const PublicRoomsResponse& QueryPublicRoomsJob::data() const BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 4d15a30b..ee33dac2 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -16,15 +16,11 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetLoginFlowsJob::LoginFlow operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetLoginFlowsJob::LoginFlow& result) { - GetLoginFlowsJob::LoginFlow result; - result.type = - fromJson(jo.value("type"_ls)); - - return result; + fromJson(jo.value("type"_ls), result.type); } }; } // namespace QMatrixClient @@ -60,7 +56,7 @@ const QVector& GetLoginFlowsJob::flows() const BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->flows = fromJson>(json.value("flows"_ls)); + fromJson(json.value("flows"_ls), d->flows); return Success; } @@ -118,10 +114,10 @@ const QString& LoginJob::deviceId() const BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->userId = fromJson(json.value("user_id"_ls)); - d->accessToken = fromJson(json.value("access_token"_ls)); - d->homeServer = fromJson(json.value("home_server"_ls)); - d->deviceId = fromJson(json.value("device_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("access_token"_ls), d->accessToken); + fromJson(json.value("home_server"_ls), d->homeServer); + fromJson(json.value("device_id"_ls), d->deviceId); return Success; } diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index c59a51ab..9aca7ec9 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -68,9 +68,9 @@ RoomEvents&& GetRoomEventsJob::chunk() BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson(json.value("start"_ls)); - d->end = fromJson(json.value("end"_ls)); - d->chunk = fromJson(json.value("chunk"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index 785a0a8a..c00b7cb0 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -16,25 +16,16 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetNotificationsJob::Notification operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetNotificationsJob::Notification& result) { - GetNotificationsJob::Notification result; - result.actions = - fromJson>(jo.value("actions"_ls)); - result.event = - fromJson(jo.value("event"_ls)); - result.profileTag = - fromJson(jo.value("profile_tag"_ls)); - result.read = - fromJson(jo.value("read"_ls)); - result.roomId = - fromJson(jo.value("room_id"_ls)); - result.ts = - fromJson(jo.value("ts"_ls)); - - return result; + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("event"_ls), result.event); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("read"_ls), result.read); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("ts"_ls), result.ts); } }; } // namespace QMatrixClient @@ -87,11 +78,11 @@ std::vector&& GetNotificationsJob::notificati BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->nextToken = fromJson(json.value("next_token"_ls)); + fromJson(json.value("next_token"_ls), d->nextToken); if (!json.contains("notifications"_ls)) return { JsonParseError, "The key 'notifications' not found in the response" }; - d->notifications = fromJson>(json.value("notifications"_ls)); + fromJson(json.value("notifications"_ls), d->notifications); return Success; } diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 2547f0c8..b27fe0b8 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -59,19 +59,19 @@ BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data) if (!json.contains("access_token"_ls)) return { JsonParseError, "The key 'access_token' not found in the response" }; - d->accessToken = fromJson(json.value("access_token"_ls)); + fromJson(json.value("access_token"_ls), d->accessToken); if (!json.contains("token_type"_ls)) return { JsonParseError, "The key 'token_type' not found in the response" }; - d->tokenType = fromJson(json.value("token_type"_ls)); + fromJson(json.value("token_type"_ls), d->tokenType); if (!json.contains("matrix_server_name"_ls)) return { JsonParseError, "The key 'matrix_server_name' not found in the response" }; - d->matrixServerName = fromJson(json.value("matrix_server_name"_ls)); + fromJson(json.value("matrix_server_name"_ls), d->matrixServerName); if (!json.contains("expires_in"_ls)) return { JsonParseError, "The key 'expires_in' not found in the response" }; - d->expiresIn = fromJson(json.value("expires_in"_ls)); + fromJson(json.value("expires_in"_ls), d->expiresIn); return Success; } diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index e046a62e..3208d48d 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -66,9 +66,9 @@ RoomEvents&& PeekEventsJob::chunk() BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson(json.value("start"_ls)); - d->end = fromJson(json.value("end"_ls)); - d->chunk = fromJson(json.value("chunk"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 7aba8b61..94a3e6c5 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -76,10 +76,10 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) if (!json.contains("presence"_ls)) return { JsonParseError, "The key 'presence' not found in the response" }; - d->presence = fromJson(json.value("presence"_ls)); - d->lastActiveAgo = fromJson(json.value("last_active_ago"_ls)); - d->statusMsg = fromJson(json.value("status_msg"_ls)); - d->currentlyActive = fromJson(json.value("currently_active"_ls)); + fromJson(json.value("presence"_ls), d->presence); + fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo); + fromJson(json.value("status_msg"_ls), d->statusMsg); + fromJson(json.value("currently_active"_ls), d->currentlyActive); return Success; } @@ -125,7 +125,7 @@ Events&& GetPresenceForListJob::data() BaseJob::Status GetPresenceForListJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index bb053062..4ed3ad9b 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -54,7 +54,7 @@ const QString& GetDisplayNameJob::displayname() const BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->displayname = fromJson(json.value("displayname"_ls)); + fromJson(json.value("displayname"_ls), d->displayname); return Success; } @@ -100,7 +100,7 @@ const QString& GetAvatarUrlJob::avatarUrl() const BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->avatarUrl = fromJson(json.value("avatar_url"_ls)); + fromJson(json.value("avatar_url"_ls), d->avatarUrl); return Success; } @@ -141,8 +141,8 @@ const QString& GetUserProfileJob::displayname() const BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->avatarUrl = fromJson(json.value("avatar_url"_ls)); - d->displayname = fromJson(json.value("displayname"_ls)); + fromJson(json.value("avatar_url"_ls), d->avatarUrl); + fromJson(json.value("displayname"_ls), d->displayname); return Success; } diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index d20db88a..3ad0dcbe 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -16,43 +16,27 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetPushersJob::PusherData operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetPushersJob::PusherData& result) { - GetPushersJob::PusherData result; - result.url = - fromJson(jo.value("url"_ls)); - result.format = - fromJson(jo.value("format"_ls)); - - return result; + fromJson(jo.value("url"_ls), result.url); + fromJson(jo.value("format"_ls), result.format); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetPushersJob::Pusher operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) { - GetPushersJob::Pusher result; - result.pushkey = - fromJson(jo.value("pushkey"_ls)); - result.kind = - fromJson(jo.value("kind"_ls)); - result.appId = - fromJson(jo.value("app_id"_ls)); - result.appDisplayName = - fromJson(jo.value("app_display_name"_ls)); - result.deviceDisplayName = - fromJson(jo.value("device_display_name"_ls)); - result.profileTag = - fromJson(jo.value("profile_tag"_ls)); - result.lang = - fromJson(jo.value("lang"_ls)); - result.data = - fromJson(jo.value("data"_ls)); - - return result; + fromJson(jo.value("pushkey"_ls), result.pushkey); + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("app_id"_ls), result.appId); + fromJson(jo.value("app_display_name"_ls), result.appDisplayName); + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("lang"_ls), result.lang); + fromJson(jo.value("data"_ls), result.data); } }; } // namespace QMatrixClient @@ -88,7 +72,7 @@ const QVector& GetPushersJob::pushers() const BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->pushers = fromJson>(json.value("pushers"_ls)); + fromJson(json.value("pushers"_ls), d->pushers); return Success; } @@ -96,13 +80,14 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const PostPusherJob::PusherData& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam(jo, QStringLiteral("url"), pod.url); - addParam(jo, QStringLiteral("format"), pod.format); - return jo; - } + static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) + { + addParam(jo, QStringLiteral("url"), pod.url); + addParam(jo, QStringLiteral("format"), pod.format); + } + }; } // namespace QMatrixClient static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index ea8ad02a..b91d18f7 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data) if (!json.contains("global"_ls)) return { JsonParseError, "The key 'global' not found in the response" }; - d->global = fromJson(json.value("global"_ls)); + fromJson(json.value("global"_ls), d->global); return Success; } @@ -80,7 +80,7 @@ const PushRule& GetPushRuleJob::data() const BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -154,7 +154,7 @@ BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data) if (!json.contains("enabled"_ls)) return { JsonParseError, "The key 'enabled' not found in the response" }; - d->enabled = fromJson(json.value("enabled"_ls)); + fromJson(json.value("enabled"_ls), d->enabled); return Success; } @@ -203,7 +203,7 @@ BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data) if (!json.contains("actions"_ls)) return { JsonParseError, "The key 'actions' not found in the response" }; - d->actions = fromJson(json.value("actions"_ls)); + fromJson(json.value("actions"_ls), d->actions); return Success; } diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 64098670..1d54e36d 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -40,7 +40,7 @@ const QString& RedactEventJob::eventId() const BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 320ec796..2772cd57 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -76,10 +76,10 @@ BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data) if (!json.contains("user_id"_ls)) return { JsonParseError, "The key 'user_id' not found in the response" }; - d->userId = fromJson(json.value("user_id"_ls)); - d->accessToken = fromJson(json.value("access_token"_ls)); - d->homeServer = fromJson(json.value("home_server"_ls)); - d->deviceId = fromJson(json.value("device_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("access_token"_ls), d->accessToken); + fromJson(json.value("home_server"_ls), d->homeServer); + fromJson(json.value("device_id"_ls), d->deviceId); return Success; } @@ -114,7 +114,7 @@ const Sid& RequestTokenToRegisterEmailJob::data() const BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -150,7 +150,7 @@ const Sid& RequestTokenToRegisterMSISDNJob::data() const BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -197,7 +197,7 @@ const Sid& RequestTokenToResetPasswordEmailJob::data() const BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -233,7 +233,7 @@ const Sid& RequestTokenToResetPasswordMSISDNJob::data() const BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -289,7 +289,7 @@ bool CheckUsernameAvailabilityJob::available() const BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->available = fromJson(json.value("available"_ls)); + fromJson(json.value("available"_ls), d->available); return Success; } diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 2b39ede2..0d25eb69 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -38,7 +38,7 @@ const QString& SendMessageJob::eventId() const BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 8f87979d..3aa7d736 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -38,7 +38,7 @@ const QString& SetRoomStateWithKeyJob::eventId() const BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } @@ -68,7 +68,7 @@ const QString& SetRoomStateJob::eventId() const BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index cebb295a..0b08ccec 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -42,7 +42,7 @@ EventPtr&& GetOneRoomEventJob::data() BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -104,7 +104,7 @@ StateEvents&& GetRoomStateJob::data() BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -114,17 +114,28 @@ class GetMembersByRoomJob::Private EventsArray chunk; }; -QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) +{ + BaseJob::Query _q; + addParam(_q, QStringLiteral("at"), at); + addParam(_q, QStringLiteral("membership"), membership); + addParam(_q, QStringLiteral("not_membership"), notMembership); + return _q; +} + +QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/members"); + basePath % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)); } static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob"); -GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId) +GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, GetMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/members") + basePath % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)) , d(new Private) { } @@ -139,7 +150,7 @@ EventsArray&& GetMembersByRoomJob::chunk() BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->chunk = fromJson>(json.value("chunk"_ls)); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } @@ -147,17 +158,12 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetJoinedMembersByRoomJob::RoomMember operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetJoinedMembersByRoomJob::RoomMember& result) { - GetJoinedMembersByRoomJob::RoomMember result; - result.displayName = - fromJson(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace QMatrixClient @@ -193,7 +199,7 @@ const QHash& GetJoinedMembersByR BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->joined = fromJson>(json.value("joined"_ls)); + fromJson(json.value("joined"_ls), d->joined); return Success; } diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 80895b4e..415aa4ed 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -158,8 +158,17 @@ namespace QMatrixClient /*! Get the m.room.member events for the room. * \param roomId * The room to get the member events for. + * \param at + * The token defining the timeline position as-of which to return + * the list of members. This token can be obtained from + * a ``prev_batch`` token returned for each room by the sync API, or + * from a ``start`` or ``end`` token returned by a /messages request. + * \param membership + * Only return users with the specified membership + * \param notMembership + * Only return users with membership state other than specified */ - explicit GetMembersByRoomJob(const QString& roomId); + explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); /*! Construct a URL without creating a full-fledged job object * @@ -167,7 +176,7 @@ namespace QMatrixClient * GetMembersByRoomJob is necessary but the job * itself isn't. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); ~GetMembersByRoomJob() override; diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 9436eb47..a5f83c79 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -16,146 +16,113 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const SearchJob::IncludeEventContext& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam(jo, QStringLiteral("before_limit"), pod.beforeLimit); - addParam(jo, QStringLiteral("after_limit"), pod.afterLimit); - addParam(jo, QStringLiteral("include_profile"), pod.includeProfile); - return jo; - } - - QJsonObject toJson(const SearchJob::Group& pod) - { - QJsonObject jo; - addParam(jo, QStringLiteral("key"), pod.key); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::IncludeEventContext& pod) + { + addParam(jo, QStringLiteral("before_limit"), pod.beforeLimit); + addParam(jo, QStringLiteral("after_limit"), pod.afterLimit); + addParam(jo, QStringLiteral("include_profile"), pod.includeProfile); + } + }; - QJsonObject toJson(const SearchJob::Groupings& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam(jo, QStringLiteral("group_by"), pod.groupBy); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) + { + addParam(jo, QStringLiteral("key"), pod.key); + } + }; - QJsonObject toJson(const SearchJob::RoomEventsCriteria& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); - addParam(jo, QStringLiteral("keys"), pod.keys); - addParam(jo, QStringLiteral("filter"), pod.filter); - addParam(jo, QStringLiteral("order_by"), pod.orderBy); - addParam(jo, QStringLiteral("event_context"), pod.eventContext); - addParam(jo, QStringLiteral("include_state"), pod.includeState); - addParam(jo, QStringLiteral("groupings"), pod.groupings); - return jo; - } - - QJsonObject toJson(const SearchJob::Categories& pod) + template <> struct JsonObjectConverter { - QJsonObject jo; - addParam(jo, QStringLiteral("room_events"), pod.roomEvents); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) + { + addParam(jo, QStringLiteral("group_by"), pod.groupBy); + } + }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchJob::UserProfile operator()(const QJsonObject& jo) const + static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) { - SearchJob::UserProfile result; - result.displayname = - fromJson(jo.value("displayname"_ls)); - result.avatarUrl = - fromJson(jo.value("avatar_url"_ls)); + addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); + addParam(jo, QStringLiteral("keys"), pod.keys); + addParam(jo, QStringLiteral("filter"), pod.filter); + addParam(jo, QStringLiteral("order_by"), pod.orderBy); + addParam(jo, QStringLiteral("event_context"), pod.eventContext); + addParam(jo, QStringLiteral("include_state"), pod.includeState); + addParam(jo, QStringLiteral("groupings"), pod.groupings); + } + }; - return result; + template <> struct JsonObjectConverter + { + static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) + { + addParam(jo, QStringLiteral("room_events"), pod.roomEvents); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchJob::EventContext operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) { - SearchJob::EventContext result; - result.begin = - fromJson(jo.value("start"_ls)); - result.end = - fromJson(jo.value("end"_ls)); - result.profileInfo = - fromJson>(jo.value("profile_info"_ls)); - result.eventsBefore = - fromJson(jo.value("events_before"_ls)); - result.eventsAfter = - fromJson(jo.value("events_after"_ls)); - - return result; + fromJson(jo.value("displayname"_ls), result.displayname); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchJob::Result operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) { - SearchJob::Result result; - result.rank = - fromJson(jo.value("rank"_ls)); - result.result = - fromJson(jo.value("result"_ls)); - result.context = - fromJson(jo.value("context"_ls)); - - return result; + fromJson(jo.value("start"_ls), result.begin); + fromJson(jo.value("end"_ls), result.end); + fromJson(jo.value("profile_info"_ls), result.profileInfo); + fromJson(jo.value("events_before"_ls), result.eventsBefore); + fromJson(jo.value("events_after"_ls), result.eventsAfter); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchJob::GroupValue operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) { - SearchJob::GroupValue result; - result.nextBatch = - fromJson(jo.value("next_batch"_ls)); - result.order = - fromJson(jo.value("order"_ls)); - result.results = - fromJson(jo.value("results"_ls)); - - return result; + fromJson(jo.value("rank"_ls), result.rank); + fromJson(jo.value("result"_ls), result.result); + fromJson(jo.value("context"_ls), result.context); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchJob::ResultRoomEvents operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) { - SearchJob::ResultRoomEvents result; - result.count = - fromJson(jo.value("count"_ls)); - result.highlights = - fromJson(jo.value("highlights"_ls)); - result.results = - fromJson>(jo.value("results"_ls)); - result.state = - fromJson>(jo.value("state"_ls)); - result.groups = - fromJson>>(jo.value("groups"_ls)); - result.nextBatch = - fromJson(jo.value("next_batch"_ls)); - - return result; + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("order"_ls), result.order); + fromJson(jo.value("results"_ls), result.results); } }; - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchJob::ResultCategories operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::ResultRoomEvents& result) { - SearchJob::ResultCategories result; - result.roomEvents = - fromJson(jo.value("room_events"_ls)); + fromJson(jo.value("count"_ls), result.count); + fromJson(jo.value("highlights"_ls), result.highlights); + fromJson(jo.value("results"_ls), result.results); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("groups"_ls), result.groups); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + } + }; - return result; + template <> struct JsonObjectConverter + { + static void fillFrom(const QJsonObject& jo, SearchJob::ResultCategories& result) + { + fromJson(jo.value("room_events"_ls), result.roomEvents); } }; } // namespace QMatrixClient @@ -199,7 +166,7 @@ BaseJob::Status SearchJob::parseJson(const QJsonDocument& data) if (!json.contains("search_categories"_ls)) return { JsonParseError, "The key 'search_categories' not found in the response" }; - d->searchCategories = fromJson(json.value("search_categories"_ls)); + fromJson(json.value("search_categories"_ls), d->searchCategories); return Success; } diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index 808915ac..94026bb9 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -16,16 +16,12 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - GetRoomTagsJob::Tag operator()(QJsonObject jo) const + static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) { - GetRoomTagsJob::Tag result; - result.order = - fromJson(jo.take("order"_ls)); - - result.additionalProperties = fromJson(jo); - return result; + fromJson(jo.take("order"_ls), result.order); + fromJson(jo, result.additionalProperties); } }; } // namespace QMatrixClient @@ -61,7 +57,7 @@ const QHash& GetRoomTagsJob::tags() const BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->tags = fromJson>(json.value("tags"_ls)); + fromJson(json.value("tags"_ls), d->tags); return Success; } diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 3ba1a5ad..12cb7c59 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -42,7 +42,7 @@ const QHash& GetProtocolsJob::data() const BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson>(data); + fromJson(data, d->data); return Success; } @@ -76,7 +76,7 @@ const ThirdPartyProtocol& GetProtocolMetadataJob::data() const BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } @@ -119,7 +119,7 @@ const QVector& QueryLocationByProtocolJob::data() const BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data) { - d->data = fromJson>(data); + fromJson(data, d->data); return Success; } @@ -162,7 +162,7 @@ const QVector& QueryUserByProtocolJob::data() const BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data) { - d->data = fromJson>(data); + fromJson(data, d->data); return Success; } @@ -205,7 +205,7 @@ const QVector& QueryLocationByAliasJob::data() const BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data) { - d->data = fromJson>(data); + fromJson(data, d->data); return Success; } @@ -248,7 +248,7 @@ const QVector& QueryUserByIDJob::data() const BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data) { - d->data = fromJson>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index deb9cb8a..97d8962d 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -16,19 +16,13 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - SearchUserDirectoryJob::User operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchUserDirectoryJob::User& result) { - SearchUserDirectoryJob::User result; - result.userId = - fromJson(jo.value("user_id"_ls)); - result.displayName = - fromJson(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace QMatrixClient @@ -71,11 +65,11 @@ BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data) if (!json.contains("results"_ls)) return { JsonParseError, "The key 'results' not found in the response" }; - d->results = fromJson>(json.value("results"_ls)); + fromJson(json.value("results"_ls), d->results); if (!json.contains("limited"_ls)) return { JsonParseError, "The key 'limited' not found in the response" }; - d->limited = fromJson(json.value("limited"_ls)); + fromJson(json.value("limited"_ls), d->limited); return Success; } diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 128902e2..c853ec06 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -43,7 +43,7 @@ const QStringList& GetVersionsJob::versions() const BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->versions = fromJson(json.value("versions"_ls)); + fromJson(json.value("versions"_ls), d->versions); return Success; } diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 0479b645..e8158723 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -42,7 +42,7 @@ const QJsonObject& GetTurnServerJob::data() const BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data) { - d->data = fromJson(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index d42534a0..97505830 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -52,8 +52,8 @@ BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) if (!json.contains("m.homeserver"_ls)) return { JsonParseError, "The key 'm.homeserver' not found in the response" }; - d->homeserver = fromJson(json.value("m.homeserver"_ls)); - d->identityServer = fromJson(json.value("m.identity_server"_ls)); + fromJson(json.value("m.homeserver"_ls), d->homeserver); + fromJson(json.value("m.identity_server"_ls), d->identityServer); return Success; } diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index cb6439ef..aebdf5d3 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) if (!json.contains("user_id"_ls)) return { JsonParseError, "The key 'user_id' not found in the response" }; - d->userId = fromJson(json.value("user_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); return Success; } diff --git a/lib/csapi/{{base}}.cpp.mustache b/lib/csapi/{{base}}.cpp.mustache index 64fd8bf3..ff888d76 100644 --- a/lib/csapi/{{base}}.cpp.mustache +++ b/lib/csapi/{{base}}.cpp.mustache @@ -8,49 +8,52 @@ {{/operations}} using namespace QMatrixClient; {{#models.model}}{{#in?}} -QJsonObject QMatrixClient::toJson(const {{qualifiedName}}& pod) +void JsonObjectConverter<{{qualifiedName}}>::dumpTo( + QJsonObject& jo, const {{qualifiedName}}& pod) { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; -} +{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); +{{/propertyMap}}{{#parents}} fillJson<{{name}}>(jo, pod); +{{/parents}}{{#vars}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); +{{/vars}}}{{!<- dumpTo() ends here}} {{/in?}}{{#out?}} -{{qualifiedName}} FromJsonObject<{{qualifiedName}}>::operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const +void JsonObjectConverter<{{qualifiedName}}>::fillFrom( + {{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); +{{#parents}} fillFromJson<{{qualifiedName}}>(jo, result); +{{/parents}}{{#vars}} fromJson(jo.{{#propertyMap}}take{{/propertyMap + }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); {{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; -} + fromJson(jo, result.{{nameCamelCase}}); +{{/propertyMap}}} {{/out?}}{{/models.model}}{{#operations}} static const auto basePath = QStringLiteral("{{basePathWithoutHost}}"); {{# operation}}{{#models}} namespace QMatrixClient { // Converters -{{#model}}{{#in?}} - QJsonObject toJson(const {{qualifiedName}}& pod) - { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; - } -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{qualifiedName}}> +{{#model}} + template <> struct JsonObjectConverter<{{qualifiedName}}> { - {{qualifiedName}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const +{{#in?}} static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); -{{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; - } - }; -{{/out?}}{{/model}}} // namespace QMatrixClient +{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); + {{/propertyMap}}{{#parents}}fillJson<{{name}}>(jo, pod); + {{/parents}}{{#vars +}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); +{{/vars}} } +{{/in?}}{{#out? +}} static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) + { +{{#parents}} fillFromJson<{{qualifiedName}}{{!of the parent!}}>(jo, result); + {{/parents}}{{#vars +}} fromJson(jo.{{#propertyMap}}take{{/propertyMap + }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); +{{/vars}}{{#propertyMap}} fromJson(jo, result.{{nameCamelCase}}); +{{/propertyMap}} } +{{/out?}} }; +{{/model}}} // namespace QMatrixClient {{/ models}}{{#responses}}{{#normalResponse?}}{{#allProperties?}} class {{camelCaseOperationId}}Job::Private { @@ -109,12 +112,12 @@ BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply) }{{/ producesNonJson?}}{{^producesNonJson?}} BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data) { -{{#inlineResponse}} d->{{paramName}} = fromJson<{{dataType.name}}>(data); +{{#inlineResponse}} fromJson(data, d->{{paramName}}); {{/inlineResponse}}{{^inlineResponse}} auto json = data.object(); {{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls)) return { JsonParseError, "The key '{{baseName}}' not found in the response" }; -{{/required?}} d->{{paramName}} = fromJson<{{dataType.name}}>(json.value("{{baseName}}"_ls)); +{{/required?}} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}}); {{/properties}}{{/inlineResponse}} return Success; }{{/ producesNonJson?}} {{/allProperties?}}{{/normalResponse?}}{{/responses}}{{/operation}}{{/operations}} diff --git a/lib/csapi/{{base}}.h.mustache b/lib/csapi/{{base}}.h.mustache index 147c8607..a9c3a63a 100644 --- a/lib/csapi/{{base}}.h.mustache +++ b/lib/csapi/{{base}}.h.mustache @@ -18,14 +18,13 @@ namespace QMatrixClient {{/vars}}{{#propertyMap}}{{#description}} /// {{_}} {{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/propertyMap}} }; -{{#in?}} - QJsonObject toJson(const {{name}}& pod); -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{name}}> + template <> struct JsonObjectConverter<{{name}}> { - {{name}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const; - }; -{{/ out?}}{{/model}} + {{#in?}}static void dumpTo(QJsonObject& jo, const {{name}}& pod); + {{/in?}}{{#out?}}static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{name}}& pod); +{{/out?}} }; +{{/model}} {{/models}}{{#operations}} // Operations {{# operation}}{{#summary}} /// {{summary}}{{#description?}}{{!add a linebreak between summary and description if both exist}} diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index d1c1abc8..a99d85ac 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -36,37 +36,38 @@ namespace QMatrixClient order_type order; TagRecord (order_type order = none) : order(order) { } - explicit TagRecord(const QJsonObject& jo) + + bool operator<(const TagRecord& other) const + { + // Per The Spec, rooms with no order should be after those with order + return !order.omitted() && + (other.order.omitted() || order.value() < other.order.value()); + } + }; + + template <> struct JsonObjectConverter + { + static void fillFrom(const QJsonObject& jo, TagRecord& rec) { // Parse a float both from JSON double and JSON string because // libqmatrixclient previously used to use strings to store order. const auto orderJv = jo.value("order"_ls); if (orderJv.isDouble()) - order = fromJson(orderJv); - else if (orderJv.isString()) + rec.order = fromJson(orderJv); + if (orderJv.isString()) { bool ok; - order = orderJv.toString().toFloat(&ok); + rec.order = orderJv.toString().toFloat(&ok); if (!ok) - order = none; + rec.order = none; } } - - bool operator<(const TagRecord& other) const + static void dumpTo(QJsonObject& jo, const TagRecord& rec) { - // Per The Spec, rooms with no order should be after those with order - return !order.omitted() && - (other.order.omitted() || order.value() < other.order.value()); + addParam(jo, QStringLiteral("order"), rec.order); } }; - inline QJsonValue toJson(const TagRecord& rec) - { - QJsonObject o; - addParam(o, QStringLiteral("order"), rec.order); - return o; - } - using TagsMap = QHash; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index cd2f9149..da663392 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -57,11 +57,15 @@ namespace QMatrixClient { matrixType); } - template struct FromJsonObject> + template struct JsonConverter> { - auto operator()(const QJsonObject& jo) const + static auto load(const QJsonValue& jv) { - return loadEvent(jo); + return loadEvent(jv.toObject()); + } + static auto load(const QJsonDocument& jd) + { + return loadEvent(jd.object()); } }; } // namespace QMatrixClient diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index eaa3302c..a5ac3c5f 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -23,20 +23,17 @@ #include -using namespace QMatrixClient; - static const std::array membershipStrings = { { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), QStringLiteral("leave"), QStringLiteral("ban") } }; -namespace QMatrixClient -{ +namespace QMatrixClient { template <> - struct FromJson + struct JsonConverter { - MembershipType operator()(const QJsonValue& jv) const + static MembershipType load(const QJsonValue& jv) { const auto& membershipString = jv.toString(); for (auto it = membershipStrings.begin(); @@ -48,9 +45,10 @@ namespace QMatrixClient return MembershipType::Undefined; } }; - } +using namespace QMatrixClient; + MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) diff --git a/lib/identity/definitions/request_email_validation.cpp b/lib/identity/definitions/request_email_validation.cpp index 95088bcb..47463a8b 100644 --- a/lib/identity/definitions/request_email_validation.cpp +++ b/lib/identity/definitions/request_email_validation.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RequestEmailValidation& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const RequestEmailValidation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("email"), pod.email); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; } -RequestEmailValidation FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, RequestEmailValidation& result) { - RequestEmailValidation result; - result.clientSecret = - fromJson(jo.value("client_secret"_ls)); - result.email = - fromJson(jo.value("email"_ls)); - result.sendAttempt = - fromJson(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson(jo.value("next_link"_ls)); - - return result; + fromJson(jo.value("client_secret"_ls), result.clientSecret); + fromJson(jo.value("email"_ls), result.email); + fromJson(jo.value("send_attempt"_ls), result.sendAttempt); + fromJson(jo.value("next_link"_ls), result.nextLink); } diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index 3e72275f..eb7d8ed6 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -33,12 +33,10 @@ namespace QMatrixClient /// server will redirect the user to this URL. QString nextLink; }; - - QJsonObject toJson(const RequestEmailValidation& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - RequestEmailValidation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod); + static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod); }; } // namespace QMatrixClient diff --git a/lib/identity/definitions/request_msisdn_validation.cpp b/lib/identity/definitions/request_msisdn_validation.cpp index 125baa9c..a123d326 100644 --- a/lib/identity/definitions/request_msisdn_validation.cpp +++ b/lib/identity/definitions/request_msisdn_validation.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RequestMsisdnValidation& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const RequestMsisdnValidation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("country"), pod.country); addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; } -RequestMsisdnValidation FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, RequestMsisdnValidation& result) { - RequestMsisdnValidation result; - result.clientSecret = - fromJson(jo.value("client_secret"_ls)); - result.country = - fromJson(jo.value("country"_ls)); - result.phoneNumber = - fromJson(jo.value("phone_number"_ls)); - result.sendAttempt = - fromJson(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson(jo.value("next_link"_ls)); - - return result; + fromJson(jo.value("client_secret"_ls), result.clientSecret); + fromJson(jo.value("country"_ls), result.country); + fromJson(jo.value("phone_number"_ls), result.phoneNumber); + fromJson(jo.value("send_attempt"_ls), result.sendAttempt); + fromJson(jo.value("next_link"_ls), result.nextLink); } diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index 77bea2bc..b48ed6d5 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -36,12 +36,10 @@ namespace QMatrixClient /// server will redirect the user to this URL. QString nextLink; }; - - QJsonObject toJson(const RequestMsisdnValidation& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - RequestMsisdnValidation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod); + static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod); }; } // namespace QMatrixClient diff --git a/lib/identity/definitions/sid.cpp b/lib/identity/definitions/sid.cpp index 443dbedf..1ba4b3b5 100644 --- a/lib/identity/definitions/sid.cpp +++ b/lib/identity/definitions/sid.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const Sid& pod) +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const Sid& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; } -Sid FromJsonObject::operator()(const QJsonObject& jo) const +void JsonObjectConverter::fillFrom( + const QJsonObject& jo, Sid& result) { - Sid result; - result.sid = - fromJson(jo.value("sid"_ls)); - - return result; + fromJson(jo.value("sid"_ls), result.sid); } diff --git a/lib/identity/definitions/sid.h b/lib/identity/definitions/sid.h index eae60c47..ac8c4130 100644 --- a/lib/identity/definitions/sid.h +++ b/lib/identity/definitions/sid.h @@ -19,12 +19,10 @@ namespace QMatrixClient /// must not be empty. QString sid; }; - - QJsonObject toJson(const Sid& pod); - - template <> struct FromJsonObject + template <> struct JsonObjectConverter { - Sid operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Sid& pod); + static void fillFrom(const QJsonObject& jo, Sid& pod); }; } // namespace QMatrixClient -- cgit v1.2.3 From 9628594881346c8e06594e65d3befafc310e12d5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 15:36:28 +0900 Subject: EventContent: minor cleanup --- lib/events/eventcontent.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 91d7a8c8..bedf0078 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -43,9 +43,10 @@ namespace QMatrixClient class Base { public: - explicit Base (const QJsonObject& o = {}) : originalJson(o) { } + explicit Base (QJsonObject o = {}) : originalJson(std::move(o)) { } virtual ~Base() = default; + // FIXME: make toJson() from converters.* work on base classes QJsonObject toJson() const; public: -- cgit v1.2.3 From a0053484024a85ae47dcd2b464cb15c0f85109e5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Nov 2018 19:20:45 +0900 Subject: SyncJob: accept Filter instead of QString for the filter --- lib/jobs/syncjob.cpp | 7 +++++++ lib/jobs/syncjob.h | 3 +++ 2 files changed, 10 insertions(+) diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index ac0f6685..84385b55 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -42,6 +42,13 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, setMaxRetries(std::numeric_limits::max()); } +SyncJob::SyncJob(const QString& since, const Filter& filter, + int timeout, const QString& presence) + : SyncJob(since, + QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), + timeout, presence) +{ } + BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { d.parseJson(data.object()); diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index a0a3c026..036b25d0 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -21,6 +21,7 @@ #include "basejob.h" #include "../syncdata.h" +#include "../csapi/definitions/sync_filter.h" namespace QMatrixClient { @@ -30,6 +31,8 @@ namespace QMatrixClient explicit SyncJob(const QString& since = {}, const QString& filter = {}, int timeout = -1, const QString& presence = {}); + explicit SyncJob(const QString& since, const Filter& filter, + int timeout = -1, const QString& presence = {}); SyncData &&takeData() { return std::move(d); } -- cgit v1.2.3 From 1ff8a0c26fc2738a085ca0302f0471ffa95a567e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 14 Nov 2018 07:16:05 +0900 Subject: Connection: support members lazy-loading This should cover the Connection-related part of #253. Connection gained lazyLoading/setLazyLoading accessors and the respective Q_PROPERTY. When lazy loading is on, sync() adds lazy_load_members: true to its filter. --- lib/connection.cpp | 23 +++++++++++++++++++---- lib/connection.h | 6 ++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 26c33767..52609370 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -94,6 +94,7 @@ class Connection::Private bool cacheState = true; bool cacheToBinary = SettingsGroup("libqmatrixclient") .value("cache_type").toString() != "json"; + bool lazyLoading; void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); @@ -287,11 +288,11 @@ void Connection::sync(int timeout) if (d->syncJob) return; - // Raw string: http://en.cppreference.com/w/cpp/language/string_literal - const auto filter = - QStringLiteral(R"({"room": { "timeline": { "limit": 100 } } })"); + Filter filter; + filter.room->timeline->limit = 100; + filter.room->state->lazyLoadMembers = d->lazyLoading; auto job = d->syncJob = callApi(BackgroundRequest, - d->data->lastEvent(), filter, timeout); + d->data->lastEvent(), filter, timeout); connect( job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; @@ -1181,6 +1182,20 @@ void Connection::setCacheState(bool newValue) } } +bool QMatrixClient::Connection::lazyLoading() const +{ + return d->lazyLoading; +} + +void QMatrixClient::Connection::setLazyLoading(bool newValue) +{ + if (d->lazyLoading != newValue) + { + d->lazyLoading = newValue; + emit lazyLoadingChanged(); + } +} + void Connection::getTurnServers() { auto job = callApi(); diff --git a/lib/connection.h b/lib/connection.h index 32533b6e..220f6c8f 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -122,6 +122,8 @@ namespace QMatrixClient Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) + Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + public: // Room ids, rather than room pointers, are used in the direct chat // map types because the library keeps Invite rooms separate from @@ -308,6 +310,9 @@ namespace QMatrixClient bool cacheState() const; void setCacheState(bool newValue); + bool lazyLoading() const; + void setLazyLoading(bool newValue); + /** Start a job of a specified type with specified arguments and policy * * This is a universal method to start a job of a type passed @@ -655,6 +660,7 @@ namespace QMatrixClient IgnoredUsersList removals); void cacheStateChanged(); + void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); protected: -- cgit v1.2.3 From 9272d21ce6e5439444794e6da58e08421e8973db Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 15:37:16 +0900 Subject: Room summaries --- lib/connection.cpp | 2 +- lib/room.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++++++- lib/room.h | 8 ++++++++ lib/syncdata.cpp | 29 +++++++++++++++++++++++++++++ lib/syncdata.h | 19 +++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 52609370..28156d11 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -562,7 +562,7 @@ void Connection::doInDirectChat(User* u, { Q_ASSERT(r->id() == roomId); // A direct chat with yourself should only involve yourself :) - if (userId == d->userId && r->memberCount() > 1) + if (userId == d->userId && r->totalMemberCount() > 1) continue; qCDebug(MAIN) << "Requested direct chat with" << userId << "is already available as" << r->id(); diff --git a/lib/room.cpp b/lib/room.cpp index 8b81bfb2..439baeb5 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -93,6 +93,7 @@ class Room::Private Connection* connection; QString id; JoinState joinState; + RoomSummary summary; /// The state of the room at timeline position before-0 /// \sa timelineBase std::unordered_map baseState; @@ -164,6 +165,8 @@ class Room::Private const RoomMessageEvent* getEventWithFile(const QString& eventId) const; QString fileNameToDownload(const RoomMessageEvent* event) const; + Changes setSummary(RoomSummary&& newSummary); + //void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); void renameMember(User* u, QString oldName); @@ -596,6 +599,10 @@ void Room::setDisplayed(bool displayed) { resetHighlightCount(); resetNotificationCount(); +// if (d->lazyLoaded) +// { +// // TODO: Get all members +// } } } @@ -976,11 +983,41 @@ bool Room::usesEncryption() const return !d->getCurrentState()->algorithm().isEmpty(); } +int Room::joinedCount() const +{ + return d->summary.joinedMemberCount > 0 + ? d->summary.joinedMemberCount + : d->membersMap.size(); +} + +int Room::invitedCount() const +{ + // TODO: Store invited users in Room too + return d->summary.invitedMemberCount; +} + +int Room::totalMemberCount() const +{ + return joinedCount() + invitedCount(); +} + GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; } +Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) +{ + if (summary == newSummary) + return Change::NoChange; + summary = move(newSummary); + qCDebug(MAIN).nospace() + << "Updated room summary: joined " << summary.joinedMemberCount + << ", invited " << summary.invitedMemberCount + << ", heroes: " << summary.heroes.join(','); + return Change::SummaryChange; +} + void Room::Private::insertMemberIntoMap(User *u) { const auto userName = u->name(q); @@ -1148,6 +1185,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (roomChanges&NameChange) emit namesChanged(this); + d->setSummary(move(data.summary)); d->updateDisplayname(); for( auto&& ephemeralEvent: data.ephemeral ) @@ -2073,7 +2111,14 @@ QString Room::Private::calculateDisplayname() const // return q->aliases().at(0); // 3. Room members - dispName = roomNameFromMemberNames(membersMap.values()); + if (!summary.heroes.empty()) + { + QList users; users.reserve(summary.heroes.size()); + for (const auto& h: summary.heroes) + users.push_back(q->user(h)); + dispName = roomNameFromMemberNames(users); + } else + dispName = roomNameFromMemberNames(membersMap.values()); if (!dispName.isEmpty()) return dispName; @@ -2103,6 +2148,7 @@ QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); QJsonObject result; + addParam(result, QStringLiteral("summary"), summary); { QJsonArray stateEvents; diff --git a/lib/room.h b/lib/room.h index 9d4561e5..97d8454a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -84,6 +84,9 @@ namespace QMatrixClient Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) + Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) + Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) + Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) @@ -116,6 +119,7 @@ namespace QMatrixClient MembersChange = 0x80, EncryptionOn = 0x100, AccountDataChange = 0x200, + SummaryChange = 0x400, OtherChange = 0x1000, AnyChange = 0x1FFF }; @@ -143,9 +147,13 @@ namespace QMatrixClient Q_INVOKABLE QList users() const; QStringList memberNames() const; + [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] int memberCount() const; int timelineSize() const; bool usesEncryption() const; + int joinedCount() const; + int invitedCount() const; + int totalMemberCount() const; GetRoomEventsJob* eventsHistoryJob() const; diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 1023ed6a..a5f849b3 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -34,10 +34,39 @@ inline EventsArrayT load(const QJsonObject& batches, StrT keyName) return fromJson(batches[keyName].toObject().value("events"_ls)); } +void JsonObjectConverter::dumpTo(QJsonObject& jo, + const RoomSummary& rs) +{ + if (rs.joinedMemberCount != 0) + jo.insert(QStringLiteral("m.joined_member_count"), + rs.joinedMemberCount); + if (rs.invitedMemberCount != 0) + jo.insert(QStringLiteral("m.invited_member_count"), + rs.invitedMemberCount); + if (!rs.heroes.empty()) + jo.insert(QStringLiteral("m.heroes"), toJson(rs.heroes)); +} + +void JsonObjectConverter::fillFrom(const QJsonObject& jo, + RoomSummary& rs) +{ + rs.joinedMemberCount = fromJson(jo["m.joined_member_count"_ls]); + rs.joinedMemberCount = fromJson(jo["m.invited_member_count"_ls]); + rs.heroes = fromJson(jo["m.heroes"]); +} + +bool RoomSummary::operator==(const RoomSummary& other) const +{ + return joinedMemberCount == other.joinedMemberCount && + invitedMemberCount == other.invitedMemberCount && + heroes == other.heroes; +} + SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, const QJsonObject& room_) : roomId(roomId_) , joinState(joinState_) + , summary(fromJson(room_["summary"].toObject())) , state(load(room_, joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) { diff --git a/lib/syncdata.h b/lib/syncdata.h index aa8948bc..81a91ffc 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -22,11 +22,30 @@ #include "events/stateevent.h" namespace QMatrixClient { + struct RoomSummary + { + int joinedMemberCount = 0; + int invitedMemberCount = 0; + QStringList heroes; //< mxids of users to take part in the room name + + bool operator==(const RoomSummary& other) const; + bool operator!=(const RoomSummary& other) const + { return !(*this == other); } + }; + + template <> + struct JsonObjectConverter + { + static void dumpTo(QJsonObject& jo, const RoomSummary& rs); + static void fillFrom(const QJsonObject& jo, RoomSummary& rs); + }; + class SyncRoomData { public: QString roomId; JoinState joinState; + RoomSummary summary; StateEvents state; RoomEvents timeline; Events ephemeral; -- cgit v1.2.3 From 4a252aa465c7a36268e8014674800e6d98a449e9 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 22:39:38 +0900 Subject: Omittable<>::merge<> --- lib/util.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/util.h b/lib/util.h index 0066c03d..9c9a37ba 100644 --- a/lib/util.h +++ b/lib/util.h @@ -118,6 +118,22 @@ namespace QMatrixClient _omitted = false; return _value; } + /// Merge the value from another Omittable + /// \return true if \p other is not omitted and the value of + /// the current Omittable was different (or omitted); + /// in other words, if the current Omittable has changed; + /// false otherwise + template + auto merge(const Omittable& other) + -> std::enable_if_t::value, bool> + { + if (other.omitted() || + (!_omitted && _value == other.value())) + return false; + _omitted = false; + _value = other.value(); + return true; + } value_type&& release() { _omitted = true; return std::move(_value); } operator value_type&() & { return editValue(); } -- cgit v1.2.3 From dae8dc138f29fce19ae666eb152567a566abe229 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 9 Dec 2018 17:41:16 +0900 Subject: JoinState: use unsigned int as the underlying type --- lib/joinstate.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/joinstate.h b/lib/joinstate.h index c172f576..379183f6 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -24,7 +24,7 @@ namespace QMatrixClient { - enum class JoinState + enum class JoinState : unsigned int { Join = 0x1, Invite = 0x2, -- cgit v1.2.3 From d51684b759f686b2c9895c5013dce88fead9661b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 8 Dec 2018 22:42:25 +0900 Subject: MSC 688: MSC-compliant RoomSummary; update Room::calculateDisplayname() The members of the summary can be omitted in the payload; this change fixes calculation of the roomname from hero names passed in room summary. Also: RoomSummary can be dumped to QDebug now. --- lib/room.cpp | 126 ++++++++++++++++++++++++++++++------------------------- lib/syncdata.cpp | 57 ++++++++++++++++--------- lib/syncdata.h | 23 +++++++--- 3 files changed, 123 insertions(+), 83 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 439baeb5..fec2dc18 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -263,8 +263,11 @@ class Room::Private QJsonObject toJson() const; private: + using users_shortlist_t = std::array; + template + users_shortlist_t buildShortlist(const ContT& users) const; + users_shortlist_t buildShortlist(const QStringList& userIds) const; QString calculateDisplayname() const; - QString roomNameFromMemberNames(const QList& userlist) const; bool isLocalUser(const User* u) const { @@ -985,9 +988,9 @@ bool Room::usesEncryption() const int Room::joinedCount() const { - return d->summary.joinedMemberCount > 0 - ? d->summary.joinedMemberCount - : d->membersMap.size(); + return d->summary.joinedMemberCount.omitted() + ? d->membersMap.size() + : d->summary.joinedMemberCount.value(); } int Room::invitedCount() const @@ -1008,13 +1011,14 @@ GetRoomEventsJob* Room::eventsHistoryJob() const Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { - if (summary == newSummary) + if (!summary.merge(newSummary)) return Change::NoChange; summary = move(newSummary); qCDebug(MAIN).nospace() - << "Updated room summary: joined " << summary.joinedMemberCount + << "Updated room summary for" << q->objectName() + << ": joined " << summary.joinedMemberCount << ", invited " << summary.invitedMemberCount - << ", heroes: " << summary.heroes.join(','); + << ", heroes: " << summary.heroes.value().join(','); return Change::SummaryChange; } @@ -1391,7 +1395,7 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) bool Room::supportsCalls() const { - return d->membersMap.size() == 2; + return joinedCount() == 2; } void Room::inviteCall(const QString& callId, const int lifetime, @@ -2045,49 +2049,35 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) return Change::NoChange; } -QString Room::Private::roomNameFromMemberNames(const QList &userlist) const +template +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const ContT& users) const { - // This is part 3(i,ii,iii) in the room displayname algorithm described - // in the CS spec (see also Room::Private::updateDisplayname() ). - // The spec requires to sort users lexicographically by state_key (user id) - // and use disambiguated display names of two topmost users excluding - // the current one to render the name of the room. - - // std::array is the leanest C++ container - std::array first_two = { {nullptr, nullptr} }; + // To calculate room display name the spec requires to sort users + // lexicographically by state_key (user id) and use disambiguated + // display names of two topmost users excluding the current one to render + // the name of the room. The below code selects 3 topmost users, + // slightly extending the spec. + users_shortlist_t shortlist { }; // Prefill with nullptrs std::partial_sort_copy( - userlist.begin(), userlist.end(), - first_two.begin(), first_two.end(), - [this](const User* u1, const User* u2) { - // Filter out the "me" user so that it never hits the room name + users.begin(), users.end(), + shortlist.begin(), shortlist.end(), + [this] (const User* u1, const User* u2) { + // localUser(), if it's in the list, is sorted below all others return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); } ); + return shortlist; +} - // Spec extension. A single person in the chat but not the local user - // (the local user is invited). - if (userlist.size() == 1 && !isLocalUser(first_two.front()) && - joinState == JoinState::Invite) - return tr("Invitation from %1") - .arg(q->roomMembername(first_two.front())); - - // i. One-on-one chat. first_two[1] == localUser() in this case. - if (userlist.size() == 2) - return q->roomMembername(first_two[0]); - - // ii. Two users besides the current one. - if (userlist.size() == 3) - return tr("%1 and %2") - .arg(q->roomMembername(first_two[0]), - q->roomMembername(first_two[1])); - - // iii. More users. - if (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(); +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const QStringList& userIds) const +{ + QList users; + users.reserve(userIds.size()); + for (const auto& h: userIds) + users.push_back(q->user(h)); + return buildShortlist(users); } QString Room::Private::calculateDisplayname() const @@ -2110,22 +2100,42 @@ QString Room::Private::calculateDisplayname() const //if (!q->aliases().empty() && !q->aliases().at(0).isEmpty()) // return q->aliases().at(0); - // 3. Room members - if (!summary.heroes.empty()) + // Supplementary code for 3 and 4: build the shortlist of users whose names + // will be used to construct the room name. Takes into account MSC688's + // "heroes" if available. + + const bool emptyRoom = membersMap.isEmpty() || + (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); + const auto shortlist = + !summary.heroes.omitted() ? buildShortlist(summary.heroes.value()) : + !emptyRoom ? buildShortlist(membersMap) : + buildShortlist(membersLeft); + + QStringList names; + for (auto u: shortlist) { - QList users; users.reserve(summary.heroes.size()); - for (const auto& h: summary.heroes) - users.push_back(q->user(h)); - dispName = roomNameFromMemberNames(users); - } else - dispName = roomNameFromMemberNames(membersMap.values()); - if (!dispName.isEmpty()) - return dispName; + if (u == nullptr || isLocalUser(u)) + break; + names.push_back(q->roomMembername(u)); + } + + auto usersCountExceptLocal = emptyRoom + ? membersLeft.size() - int(joinState == JoinState::Leave) + : q->joinedCount() - int(joinState == JoinState::Join); + if (usersCountExceptLocal > int(shortlist.size())) + names << + tr("%Ln other(s)", + "Used to make a room name from user names: A, B and _N others_", + usersCountExceptLocal); + auto namesList = QLocale().createSeparatedList(names); + + // 3. Room members + if (!emptyRoom) + return namesList; // 4. Users that previously left the room - dispName = roomNameFromMemberNames(membersLeft); - if (!dispName.isEmpty()) - return tr("Empty room (was: %1)").arg(dispName); + if (membersLeft.size() > 0) + return tr("Empty room (was: %1)").arg(namesList); // 5. Fail miserably return tr("Empty room (%1)").arg(id); diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index a5f849b3..f55d4396 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -28,45 +28,64 @@ using namespace QMatrixClient; const QString SyncRoomData::UnreadCountKey = QStringLiteral("x-qmatrixclient.unread_count"); -template -inline EventsArrayT load(const QJsonObject& batches, StrT keyName) +bool RoomSummary::isEmpty() const { - return fromJson(batches[keyName].toObject().value("events"_ls)); + return joinedMemberCount.omitted() && invitedMemberCount.omitted() && + heroes.omitted(); +} + +bool RoomSummary::merge(const RoomSummary& other) +{ + // Using bitwise OR to prevent computation shortcut. + return + joinedMemberCount.merge(other.joinedMemberCount) | + invitedMemberCount.merge(other.invitedMemberCount) | + heroes.merge(other.heroes); +} + +QDebug QMatrixClient::operator<<(QDebug dbg, const RoomSummary& rs) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!rs.joinedMemberCount.omitted()) + sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value()); + if (!rs.invitedMemberCount.omitted()) + sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value()); + if (!rs.heroes.omitted()) + sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(',')); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; } void JsonObjectConverter::dumpTo(QJsonObject& jo, const RoomSummary& rs) { - if (rs.joinedMemberCount != 0) - jo.insert(QStringLiteral("m.joined_member_count"), - rs.joinedMemberCount); - if (rs.invitedMemberCount != 0) - jo.insert(QStringLiteral("m.invited_member_count"), - rs.invitedMemberCount); - if (!rs.heroes.empty()) - jo.insert(QStringLiteral("m.heroes"), toJson(rs.heroes)); + addParam(jo, QStringLiteral("m.joined_member_count"), + rs.joinedMemberCount); + addParam(jo, QStringLiteral("m.invited_member_count"), + rs.invitedMemberCount); + addParam(jo, QStringLiteral("m.heroes"), rs.heroes); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, RoomSummary& rs) { - rs.joinedMemberCount = fromJson(jo["m.joined_member_count"_ls]); - rs.joinedMemberCount = fromJson(jo["m.invited_member_count"_ls]); - rs.heroes = fromJson(jo["m.heroes"]); + fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount); + fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount); + fromJson(jo["m.heroes"], rs.heroes); } -bool RoomSummary::operator==(const RoomSummary& other) const +template +inline EventsArrayT load(const QJsonObject& batches, StrT keyName) { - return joinedMemberCount == other.joinedMemberCount && - invitedMemberCount == other.invitedMemberCount && - heroes == other.heroes; + return fromJson(batches[keyName].toObject().value("events"_ls)); } SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, const QJsonObject& room_) : roomId(roomId_) , joinState(joinState_) - , summary(fromJson(room_["summary"].toObject())) + , summary(fromJson(room_["summary"])) , state(load(room_, joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) { diff --git a/lib/syncdata.h b/lib/syncdata.h index 81a91ffc..663553bc 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -22,15 +22,26 @@ #include "events/stateevent.h" namespace QMatrixClient { + /// Room summary, as defined in MSC688 + /** + * Every member of this structure is an Omittable; as per the MSC, only + * changed values are sent from the server so if nothing is in the payload + * the respective member will be omitted. In particular, `heroes.omitted()` + * means that nothing has come from the server; heroes.value().isEmpty() + * means a peculiar case of a room with the only member - the current user. + */ struct RoomSummary { - int joinedMemberCount = 0; - int invitedMemberCount = 0; - QStringList heroes; //< mxids of users to take part in the room name + Omittable joinedMemberCount; + Omittable invitedMemberCount; + Omittable heroes; //< mxids of users to take part in the room name - bool operator==(const RoomSummary& other) const; - bool operator!=(const RoomSummary& other) const - { return !(*this == other); } + bool isEmpty() const; + /// Merge the contents of another RoomSummary object into this one + /// \return true, if the current object has changed; false otherwise + bool merge(const RoomSummary& other); + + friend QDebug operator<<(QDebug dbg, const RoomSummary& rs); }; template <> -- cgit v1.2.3 From 1678296d5b6190679a9ef950f9421945fa159f8f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 9 Dec 2018 17:42:06 +0900 Subject: fromJson, fillFromJson: avoid overwriting pods if the JSON value is undefined --- lib/converters.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/converters.h b/lib/converters.h index 6227902d..af2be645 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -110,7 +110,8 @@ namespace QMatrixClient template inline void fromJson(const QJsonValue& jv, T& pod) { - pod = fromJson(jv); + if (!jv.isUndefined()) + pod = fromJson(jv); } template @@ -123,13 +124,17 @@ namespace QMatrixClient template inline void fromJson(const QJsonValue& jv, Omittable& pod) { - pod = fromJson(jv); + if (jv.isUndefined()) + pod = none; + else + pod = fromJson(jv); } template inline void fillFromJson(const QJsonValue& jv, T& pod) { - JsonObjectConverter::fillFrom(jv.toObject(), pod); + if (jv.isObject()) + JsonObjectConverter::fillFrom(jv.toObject(), pod); } // JsonConverter<> specialisations -- cgit v1.2.3 From 9225eaec426ecd44a1c203e11e1aafe7772c46d7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 9 Dec 2018 19:55:14 +0900 Subject: Room: track more changes; fix cache smashing upon restart Commit fd52459 introduced a regression rendering the cache unusable after a client restart (an empty state overwrites whatever state was in the cache). This commit contains the fix, along with more room change tracking. --- lib/room.cpp | 59 +++++++++++++++++++++++++++++++++++------------------------ lib/room.h | 7 ++++--- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index fec2dc18..ca5495ea 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -215,12 +215,12 @@ class Room::Private */ void dropDuplicateEvents(RoomEvents& events) const; - void setLastReadEvent(User* u, QString eventId); + Changes setLastReadEvent(User* u, QString eventId); void updateUnreadCount(rev_iter_t from, rev_iter_t to); - void promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); + Changes promoteReadMarker(User* u, rev_iter_t newMarker, + bool force = false); - void markMessagesAsRead(rev_iter_t upToMarker); + Changes markMessagesAsRead(rev_iter_t upToMarker); QString sendEvent(RoomEventPtr&& event); @@ -392,11 +392,11 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadEvent(User* u, QString eventId) +Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) { auto& storedId = lastReadEventIds[u]; if (storedId == eventId) - return; + return Change::NoChange; eventIdReadUsers.remove(storedId, u); eventIdReadUsers.insert(eventId, u); swap(storedId, eventId); @@ -407,8 +407,9 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi(id, storedId); emit q->readMarkerMoved(eventId, storedId); - connection->saveRoomState(q); + return Change::ReadMarkerChange; } + return Change::NoChange; } void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) @@ -451,14 +452,15 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) } } -void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) +Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, + bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators - return; + return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); @@ -467,7 +469,7 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) auto eagerMarker = find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); - setLastReadEvent(u, (*(eagerMarker - 1))->id()); + auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { const auto oldUnreadCount = unreadMessages; @@ -491,14 +493,16 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) qCDebug(MAIN) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); + changes |= Change::UnreadNotifsChange; } } + return changes; } -void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { const auto prevMarker = q->readMarker(); - promoteReadMarker(q->localUser(), upToMarker); + auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); @@ -514,6 +518,7 @@ void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) break; } } + return changes; } void Room::markMessagesAsRead(QString uptoEventId) @@ -1193,7 +1198,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) d->updateDisplayname(); for( auto&& ephemeralEvent: data.ephemeral ) - processEphemeralEvent(move(ephemeralEvent)); + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) @@ -1758,9 +1763,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // 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; + Changes roomChanges = Change::NoChange; for (const auto& eptr: events) - stateChanges |= q->processStateEvent(*eptr); + roomChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); auto totalInserted = 0; @@ -1820,16 +1825,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); + roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + roomChanges |= Change::UnreadNotifsChange; } Q_ASSERT(timeline.size() == timelineSize + totalInserted); - return stateChanges; + return roomChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1948,8 +1954,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) ); } -void Room::processEphemeralEvent(EventPtr&& event) +Room::Changes Room::processEphemeralEvent(EventPtr&& event) { + Changes changes = NoChange; QElapsedTimer et; et.start(); if (auto* evt = eventCast(event)) { @@ -1988,7 +1995,7 @@ void Room::processEphemeralEvent(EventPtr&& event) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join) - d->promoteReadMarker(u, newMarker); + changes |= d->promoteReadMarker(u, newMarker); } } else { @@ -2005,7 +2012,7 @@ void Room::processEphemeralEvent(EventPtr&& event) auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join && readMarker(u) == timelineEdge()) - d->setLastReadEvent(u, p.evtId); + changes |= d->setLastReadEvent(u, p.evtId); } } } @@ -2015,12 +2022,17 @@ void Room::processEphemeralEvent(EventPtr&& event) << evt->eventsWithReceipts().size() << "event(s) with" << totalReceipts << "receipt(s)," << et; } + return changes; } Room::Changes Room::processAccountDataEvent(EventPtr&& event) { + Changes changes = NoChange; if (auto* evt = eventCast(event)) + { d->setTags(evt->tags()); + changes |= Change::TagsChange; + } if (auto* evt = eventCast(event)) { @@ -2028,10 +2040,9 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); - if (newMarker != timelineEdge()) - d->markMessagesAsRead(newMarker); - else - d->setLastReadEvent(localUser(), readEventId); + changes |= newMarker != timelineEdge() + ? d->markMessagesAsRead(newMarker) + : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; diff --git a/lib/room.h b/lib/room.h index 97d8454a..7b5be331 100644 --- a/lib/room.h +++ b/lib/room.h @@ -120,8 +120,9 @@ namespace QMatrixClient EncryptionOn = 0x100, AccountDataChange = 0x200, SummaryChange = 0x400, - OtherChange = 0x1000, - AnyChange = 0x1FFF + ReadMarkerChange = 0x800, + OtherChange = 0x8000, + AnyChange = 0xFFFF }; Q_DECLARE_FLAGS(Changes, Change) Q_FLAG(Changes) @@ -467,7 +468,7 @@ namespace QMatrixClient protected: /// Returns true if any of room names/aliases has changed virtual Changes processStateEvent(const RoomEvent& e); - virtual void processEphemeralEvent(EventPtr&& event); + virtual Changes processEphemeralEvent(EventPtr&& event); virtual Changes processAccountDataEvent(EventPtr&& event); virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } -- cgit v1.2.3 From 9a1453bbd56fb7912c73845fc8580ce79e694286 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 9 Dec 2018 19:55:14 +0900 Subject: Room: track more changes; fix cache smashing upon restart Commit fd52459 introduced a regression rendering the cache unusable after a client restart (an empty state overwrites whatever state was in the cache). This commit contains the fix, along with more room change tracking. # Conflicts: # lib/room.h --- lib/room.cpp | 59 +++++++++++++++++++++++++++++++++++------------------------ lib/room.h | 7 ++++--- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 8b81bfb2..cdc7572a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -212,12 +212,12 @@ class Room::Private */ void dropDuplicateEvents(RoomEvents& events) const; - void setLastReadEvent(User* u, QString eventId); + Changes setLastReadEvent(User* u, QString eventId); void updateUnreadCount(rev_iter_t from, rev_iter_t to); - void promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); + Changes promoteReadMarker(User* u, rev_iter_t newMarker, + bool force = false); - void markMessagesAsRead(rev_iter_t upToMarker); + Changes markMessagesAsRead(rev_iter_t upToMarker); QString sendEvent(RoomEventPtr&& event); @@ -386,11 +386,11 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadEvent(User* u, QString eventId) +Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) { auto& storedId = lastReadEventIds[u]; if (storedId == eventId) - return; + return Change::NoChange; eventIdReadUsers.remove(storedId, u); eventIdReadUsers.insert(eventId, u); swap(storedId, eventId); @@ -401,8 +401,9 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi(id, storedId); emit q->readMarkerMoved(eventId, storedId); - connection->saveRoomState(q); + return Change::ReadMarkerChange; } + return Change::NoChange; } void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) @@ -445,14 +446,15 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) } } -void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) +Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, + bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators - return; + return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); @@ -461,7 +463,7 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) auto eagerMarker = find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); - setLastReadEvent(u, (*(eagerMarker - 1))->id()); + auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { const auto oldUnreadCount = unreadMessages; @@ -485,14 +487,16 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) qCDebug(MAIN) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); + changes |= Change::UnreadNotifsChange; } } + return changes; } -void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { const auto prevMarker = q->readMarker(); - promoteReadMarker(q->localUser(), upToMarker); + auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); @@ -508,6 +512,7 @@ void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) break; } } + return changes; } void Room::markMessagesAsRead(QString uptoEventId) @@ -1151,7 +1156,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) d->updateDisplayname(); for( auto&& ephemeralEvent: data.ephemeral ) - processEphemeralEvent(move(ephemeralEvent)); + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) @@ -1716,9 +1721,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // 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; + Changes roomChanges = Change::NoChange; for (const auto& eptr: events) - stateChanges |= q->processStateEvent(*eptr); + roomChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); auto totalInserted = 0; @@ -1778,16 +1783,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); + roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + roomChanges |= Change::UnreadNotifsChange; } Q_ASSERT(timeline.size() == timelineSize + totalInserted); - return stateChanges; + return roomChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1906,8 +1912,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) ); } -void Room::processEphemeralEvent(EventPtr&& event) +Room::Changes Room::processEphemeralEvent(EventPtr&& event) { + Changes changes = NoChange; QElapsedTimer et; et.start(); if (auto* evt = eventCast(event)) { @@ -1946,7 +1953,7 @@ void Room::processEphemeralEvent(EventPtr&& event) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join) - d->promoteReadMarker(u, newMarker); + changes |= d->promoteReadMarker(u, newMarker); } } else { @@ -1963,7 +1970,7 @@ void Room::processEphemeralEvent(EventPtr&& event) auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join && readMarker(u) == timelineEdge()) - d->setLastReadEvent(u, p.evtId); + changes |= d->setLastReadEvent(u, p.evtId); } } } @@ -1973,12 +1980,17 @@ void Room::processEphemeralEvent(EventPtr&& event) << evt->eventsWithReceipts().size() << "event(s) with" << totalReceipts << "receipt(s)," << et; } + return changes; } Room::Changes Room::processAccountDataEvent(EventPtr&& event) { + Changes changes = NoChange; if (auto* evt = eventCast(event)) + { d->setTags(evt->tags()); + changes |= Change::TagsChange; + } if (auto* evt = eventCast(event)) { @@ -1986,10 +1998,9 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); - if (newMarker != timelineEdge()) - d->markMessagesAsRead(newMarker); - else - d->setLastReadEvent(localUser(), readEventId); + changes |= newMarker != timelineEdge() + ? d->markMessagesAsRead(newMarker) + : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; diff --git a/lib/room.h b/lib/room.h index 9d4561e5..7533c599 100644 --- a/lib/room.h +++ b/lib/room.h @@ -116,8 +116,9 @@ namespace QMatrixClient MembersChange = 0x80, EncryptionOn = 0x100, AccountDataChange = 0x200, - OtherChange = 0x1000, - AnyChange = 0x1FFF + ReadMarkerChange = 0x800, + OtherChange = 0x8000, + AnyChange = 0xFFFF }; Q_DECLARE_FLAGS(Changes, Change) Q_FLAG(Changes) @@ -459,7 +460,7 @@ namespace QMatrixClient protected: /// Returns true if any of room names/aliases has changed virtual Changes processStateEvent(const RoomEvent& e); - virtual void processEphemeralEvent(EventPtr&& event); + virtual Changes processEphemeralEvent(EventPtr&& event); virtual Changes processAccountDataEvent(EventPtr&& event); virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } -- cgit v1.2.3 From 9b3e437f3268e251f1950000b210cf849d49c24e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 9 Dec 2018 20:04:45 +0900 Subject: Room: defer memberListChanged(); track room summary changes This concludes beta-version of lazy-loading support in libQMatrixClient (#253). --- lib/room.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index ca5495ea..84072d3e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -282,9 +282,6 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; - connect(this, &Room::userAdded, this, &Room::memberListChanged); - connect(this, &Room::userRemoved, this, &Room::memberListChanged); - connect(this, &Room::memberRenamed, this, &Room::memberListChanged); qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } @@ -1018,12 +1015,9 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { if (!summary.merge(newSummary)) return Change::NoChange; - summary = move(newSummary); - qCDebug(MAIN).nospace() - << "Updated room summary for" << q->objectName() - << ": joined " << summary.joinedMemberCount - << ", invited " << summary.invitedMemberCount - << ", heroes: " << summary.heroes.value().join(','); + qCDebug(MAIN).nospace().noquote() + << "Updated room summary for " << q->objectName() << ": " << summary; + emit q->memberListChanged(); return Change::SummaryChange; } @@ -1194,7 +1188,10 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (roomChanges&NameChange) emit namesChanged(this); - d->setSummary(move(data.summary)); + if (roomChanges&MembersChange) + emit memberListChanged(); + + roomChanges |= d->setSummary(move(data.summary)); d->updateDisplayname(); for( auto&& ephemeralEvent: data.ephemeral ) -- cgit v1.2.3 From 8f39d870759de362d5ac911d6554347fb3b46759 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 10 Dec 2018 07:07:15 +0900 Subject: Suppress a function_traits<> test with lambdas on MSVC2015 Assigning a lambda to a static variable causes it to fail with 'auto must always deduce to the same type' error. --- lib/util.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/util.cpp b/lib/util.cpp index 5e7644a1..e22a1df5 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -112,10 +112,12 @@ static_assert(is_callable_v, "Test is_callable<> with function object 1"); static_assert(std::is_same, int>(), "Test fn_arg_t defaulting to first argument"); +#if (!defined(_MSC_VER) || _MSC_VER >= 1910) static auto l = [] { return 1; }; static_assert(is_callable_v, "Test is_callable_v<> with lambda"); static_assert(std::is_same, int>::value, "Test fn_return_t<> with lambda"); +#endif template struct fn_object -- cgit v1.2.3 From 501c79f55b5f6cb5df80993330d0b1ae1764024a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 10 Dec 2018 16:32:33 +0900 Subject: Room::getPreviousContent: use early return ...instead of the entire function body wrapped in an if block. --- lib/room.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 84072d3e..3cbd2271 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1440,18 +1440,18 @@ void Room::getPreviousContent(int limit) void Room::Private::getPreviousContent(int limit) { - if( !isJobRunning(eventsHistoryJob) ) - { - eventsHistoryJob = - connection->callApi(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); - } + if (isJobRunning(eventsHistoryJob)) + return; + + eventsHistoryJob = + connection->callApi(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); } void Room::inviteToRoom(const QString& memberId) -- cgit v1.2.3 From c6720cc8bb8d45ab4d2b7390f076d50cb59cb8d3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 11 Dec 2018 12:23:33 +0900 Subject: Expose Connection::nextBatchToken() --- lib/connection.cpp | 5 +++++ lib/connection.h | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 28156d11..76e61ed1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -412,6 +412,11 @@ void Connection::stopSync() } } +QString Connection::nextBatchToken() const +{ + return d->data->lastEvent(); +} + PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const { return callApi(room->id(), "m.read", event->id()); diff --git a/lib/connection.h b/lib/connection.h index 220f6c8f..9a94aad6 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -390,6 +390,7 @@ namespace QMatrixClient void sync(int timeout = -1); void stopSync(); + QString nextBatchToken() const; virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; -- cgit v1.2.3 From 0e67d1e92285e0c03e4e34ad747490ae4dc5482d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 11 Dec 2018 12:26:05 +0900 Subject: RoomMemberEvent: properly integrate with GetMembersByRoomJob GetMembersByRoomJob was dysfunctional so far, creating "unknown RoomMemberEvents" instead of proper ones. Now that we need it for lazy- loading, it's fixed! --- lib/events/roommemberevent.h | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 149d74f8..b8224033 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -60,8 +60,16 @@ namespace QMatrixClient : StateEvent(typeId(), matrixTypeId(), c) { } - // This is a special constructor enabling RoomMemberEvent to be - // a base class for more specific member events. + /// A special constructor to create unknown RoomMemberEvents + /** + * This is needed in order to use RoomMemberEvent as a "base event + * class" in cases like GetMembersByRoomJob when RoomMemberEvents + * (rather than RoomEvents or StateEvents) are resolved from JSON. + * For such cases loadEvent<> requires an underlying class to be + * constructible with unknownTypeId() instead of its genuine id. + * Don't use it directly. + * \sa GetMembersByRoomJob, loadEvent, unknownTypeId + */ RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) { } @@ -81,6 +89,18 @@ namespace QMatrixClient private: REGISTER_ENUM(MembershipType) }; + + template <> + class EventFactory + { + public: + static event_ptr_tt make(const QJsonObject& json, + const QString&) + { + return makeEvent(json); + } + }; + REGISTER_EVENT_TYPE(RoomMemberEvent) DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent) } // namespace QMatrixClient -- cgit v1.2.3 From f0bd24a830aef3405994849ce413e2d488f75429 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 11 Dec 2018 12:29:57 +0900 Subject: Make Room::setDisplayed() trigger loading all members Closes #253. --- lib/room.cpp | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 3cbd2271..439bec0f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -27,6 +27,7 @@ #include "csapi/account-data.h" #include "csapi/room_state.h" #include "csapi/room_send.h" +#include "csapi/rooms.h" #include "csapi/tags.h" #include "events/simplestateevents.h" #include "events/roomavatarevent.h" @@ -121,6 +122,7 @@ class Room::Private std::unordered_map accountData; QString prevBatch; QPointer eventsHistoryJob; + QPointer allMembersJob; struct FileTransferPrivateInfo { @@ -222,6 +224,8 @@ class Room::Private Changes markMessagesAsRead(rev_iter_t upToMarker); + void getAllMembers(); + QString sendEvent(RoomEventPtr&& event); template @@ -588,6 +592,36 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const return timelineEdge(); } +void Room::Private::getAllMembers() +{ + // If already loaded or already loading, there's nothing to do here. + if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) + return; + + allMembersJob = connection->callApi( + id, connection->nextBatchToken(), "join"); + auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; + connect( allMembersJob, &BaseJob::success, q, [=] { + Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); + Changes roomChanges = NoChange; + for (auto&& e: allMembersJob->chunk()) + { + const auto& evt = *e; + baseState[{evt.matrixType(),evt.stateKey()}] = move(e); + roomChanges |= q->processStateEvent(evt); + } + // Replay member events that arrived after the point for which + // the full members list was requested. + if (!timeline.empty() ) + for (auto it = q->findInTimeline(nextIndex).base(); + it != timeline.cend(); ++it) + if (is(**it)) + roomChanges |= q->processStateEvent(**it); + if (roomChanges&MembersChange) + emit q->memberListChanged(); + }); +} + bool Room::displayed() const { return d->displayed; @@ -604,10 +638,7 @@ void Room::setDisplayed(bool displayed) { resetHighlightCount(); resetNotificationCount(); -// if (d->lazyLoaded) -// { -// // TODO: Get all members -// } + d->getAllMembers(); } } -- cgit v1.2.3 From 095444aff98ac56663bb205837a57e746d950f3b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 12 Dec 2018 13:20:05 +0900 Subject: Room::allMembersLoaded(); more doc-comments --- lib/room.cpp | 1 + lib/room.h | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 439bec0f..7232741a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -619,6 +619,7 @@ void Room::Private::getAllMembers() roomChanges |= q->processStateEvent(**it); if (roomChanges&MembersChange) emit q->memberListChanged(); + emit q->allMembersLoaded(); }); } diff --git a/lib/room.h b/lib/room.h index 7b5be331..ba1eaa48 100644 --- a/lib/room.h +++ b/lib/room.h @@ -228,6 +228,13 @@ namespace QMatrixClient rev_iter_t findInTimeline(const QString& evtId) const; bool displayed() const; + /// Mark the room as currently displayed to the user + /** + * Marking the room displayed causes the room to obtain the full + * list of members if it's been lazy-loaded before; in the future + * it may do more things bound to "screen time" of the room, e.g. + * measure that "screen time". + */ void setDisplayed(bool displayed = true); QString firstDisplayedEventId() const; rev_iter_t firstDisplayedMarker() const; @@ -431,6 +438,9 @@ namespace QMatrixClient void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); void memberListChanged(); + /// The previously lazy-loaded members list is now loaded entirely + /// \sa setDisplayed + void allMembersLoaded(); void encryption(); void joinStateChanged(JoinState oldState, JoinState newState); -- cgit v1.2.3 From c46663aece5e001543b07d3ed901e64c38be4172 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 12 Dec 2018 13:20:46 +0900 Subject: qmc-example: Use lazy-loading; check full-loading upon setDisplayed --- examples/qmc-example.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 9c86d4a9..bdb9ef3f 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -26,6 +26,7 @@ class QMCTest : public QObject void setup(const QString& testRoomName); void onNewRoom(Room* r); void startTests(); + void loadMembers(); void sendMessage(); void addAndRemoveTag(); void sendAndRedact(); @@ -80,6 +81,7 @@ void QMCTest::setup(const QString& testRoomName) cout << "Access token: " << c->accessToken().toStdString() << endl; // Setting up sync loop + c->setLazyLoading(true); c->sync(); connect(c.data(), &Connection::syncDone, c.data(), [this,testRoomName] { cout << "Sync complete, " @@ -142,12 +144,41 @@ void QMCTest::onNewRoom(Room* r) void QMCTest::startTests() { cout << "Starting tests" << endl; + loadMembers(); sendMessage(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); } +void QMCTest::loadMembers() +{ + running.push_back("Loading members"); + // The dedicated qmc-test room is too small to test + // lazy-loading-then-full-loading; use #test:matrix.org instead. + // TODO: #264 + auto* r = c->room(QStringLiteral("!vfFxDRtZSSdspfTSEr:matrix.org")); + if (!r) + { + cout << "#test:matrix.org is not found in the test user's rooms" << endl; + QMC_CHECK("Loading members", false); + return; + } + // It's not exactly correct because an arbitrary server might not support + // lazy loading; but in the absence of capabilities framework we assume + // it does. + if (r->memberNames().size() < r->joinedCount()) + { + cout << "Lazy loading doesn't seem to be enabled" << endl; + QMC_CHECK("Loading members", false); + return; + } + r->setDisplayed(); + connect(r, &Room::allMembersLoaded, [this] { + QMC_CHECK("Loading members", true); + }); +} + void QMCTest::sendMessage() { running.push_back("Message sending"); -- cgit v1.2.3 From 393485594b2bb7ab3a7ddc7e49c8cae1105bf77e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 12 Dec 2018 13:37:50 +0900 Subject: Room: more doc-comments --- lib/room.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/room.h b/lib/room.h index ba1eaa48..f7eb224e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -437,6 +437,13 @@ namespace QMatrixClient void userRemoved(User* user); void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); + /// The list of members has changed + /** Emitted no more than once per sync, this is a good signal to + * for cases when some action should be done upon any change in + * the member list. If you need per-item granularity you should use + * userAdded, userRemoved and memberAboutToRename / memberRenamed + * instead. + */ void memberListChanged(); /// The previously lazy-loaded members list is now loaded entirely /// \sa setDisplayed -- cgit v1.2.3 From c33680b62d968e1e0e2abcdc084eaecf5dd94d2f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 12 Dec 2018 16:50:00 +0900 Subject: csapi/rooms.h: regenerate to update doc-comments --- lib/csapi/rooms.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 415aa4ed..b4d3d9b6 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -160,9 +160,9 @@ namespace QMatrixClient * The room to get the member events for. * \param at * The token defining the timeline position as-of which to return - * the list of members. This token can be obtained from - * a ``prev_batch`` token returned for each room by the sync API, or - * from a ``start`` or ``end`` token returned by a /messages request. + * the list of members. This token can be obtained from a batch token + * returned for each room by the sync API, or from + * a ``start``/``end`` token returned by a ``/messages`` request. * \param membership * Only return users with the specified membership * \param notMembership -- cgit v1.2.3 From cda9a0f02cc3e779da378d0328f9d24c708b2600 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 12 Dec 2018 17:05:26 +0900 Subject: gtad.yaml: use more compact definitions where possible --- lib/csapi/gtad.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index cb5e553c..ca4a0fb9 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -86,12 +86,9 @@ analyzer: - /m\.room\.member$/: type: "EventsArray" imports: '"events/roommemberevent.h"' - - /state_event.yaml$/: - type: StateEvents - - /room_event.yaml$/: - type: RoomEvents - - /event.yaml$/: - type: Events + - /state_event.yaml$/: StateEvents + - /room_event.yaml$/: RoomEvents + - /event.yaml$/: Events - //: { type: "QVector<{{1}}>", imports: } - map: # `additionalProperties` in OpenAPI - RoomState: -- cgit v1.2.3 From 91b20cae3f60bf8c3b2b66c28911feca2d1d575d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 12 Dec 2018 17:05:26 +0900 Subject: gtad.yaml: use more compact definitions where possible --- lib/csapi/gtad.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index cb5e553c..ca4a0fb9 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -86,12 +86,9 @@ analyzer: - /m\.room\.member$/: type: "EventsArray" imports: '"events/roommemberevent.h"' - - /state_event.yaml$/: - type: StateEvents - - /room_event.yaml$/: - type: RoomEvents - - /event.yaml$/: - type: Events + - /state_event.yaml$/: StateEvents + - /room_event.yaml$/: RoomEvents + - /event.yaml$/: Events - //: { type: "QVector<{{1}}>", imports: } - map: # `additionalProperties` in OpenAPI - RoomState: -- cgit v1.2.3 From 5b06b165ba2adec50099452bcf4c5f20009423ad Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 13 Dec 2018 07:23:50 +0900 Subject: gtad.yaml: wrap bool in Omittable<> Case in point: https://github.com/matrix-org/matrix-doc/issues/1750 --- lib/csapi/gtad.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index ca4a0fb9..c6ea8a13 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -38,7 +38,7 @@ analyzer: - number: - float: float - //: double - - boolean: { type: bool, omittedValue: 'false' } + - boolean: bool - string: - byte: &ByteStream type: QIODevice* -- cgit v1.2.3 From 8dcda23ed210151904c9137067626eddae683822 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 13 Dec 2018 07:47:58 +0900 Subject: Regenerate csapi/ --- lib/csapi/administrative_contact.cpp | 2 +- lib/csapi/administrative_contact.h | 2 +- lib/csapi/create_room.cpp | 2 +- lib/csapi/create_room.h | 2 +- lib/csapi/definitions/push_rule.h | 4 ++-- lib/csapi/definitions/room_event_filter.cpp | 6 ------ lib/csapi/definitions/room_event_filter.h | 8 ++------ lib/csapi/definitions/sync_filter.h | 2 +- lib/csapi/joining.h | 4 ++-- lib/csapi/list_public_rooms.cpp | 2 +- lib/csapi/list_public_rooms.h | 2 +- lib/csapi/presence.cpp | 4 ++-- lib/csapi/presence.h | 2 +- lib/csapi/pusher.cpp | 2 +- lib/csapi/pusher.h | 2 +- lib/csapi/registration.cpp | 6 +++--- lib/csapi/registration.h | 4 ++-- lib/csapi/search.h | 4 ++-- 18 files changed, 25 insertions(+), 35 deletions(-) diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index f62002a6..20a1de42 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -86,7 +86,7 @@ namespace QMatrixClient static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); -Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind) +Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable bind) : BaseJob(HttpVerb::Post, Post3PIDsJobName, basePath % "/account/3pid") { diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 3fb3d44c..02aeee4d 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -113,7 +113,7 @@ namespace QMatrixClient * identifier to the account's Matrix ID with the passed identity * server. Default: ``false``. */ - explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = false); + explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable bind = none); }; /// Deletes a third party identifier from the user's account diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 36f83727..236daf18 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -43,7 +43,7 @@ class CreateRoomJob::Private static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob"); -CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector& initialState, const QString& preset, bool isDirect, const QJsonObject& powerLevelContentOverride) +CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector& initialState, const QString& preset, Omittable isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, CreateRoomJobName, basePath % "/createRoom") , d(new Private) diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index a0a64df0..d7c01d00 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -216,7 +216,7 @@ namespace QMatrixClient * event content prior to it being sent to the room. Defaults to * overriding nothing. */ - explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector& initialState = {}, const QString& preset = {}, bool isDirect = false, const QJsonObject& powerLevelContentOverride = {}); + explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector& initialState = {}, const QString& preset = {}, Omittable isDirect = none, const QJsonObject& powerLevelContentOverride = {}); ~CreateRoomJob() override; // Result properties diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 5f52876d..2ab68cb1 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -7,10 +7,10 @@ #include "converters.h" #include "csapi/definitions/push_condition.h" -#include "converters.h" +#include #include #include -#include +#include "converters.h" namespace QMatrixClient { diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp index 8cd2ded7..f6f1e5cb 100644 --- a/lib/csapi/definitions/room_event_filter.cpp +++ b/lib/csapi/definitions/room_event_filter.cpp @@ -12,8 +12,6 @@ QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod) addParam(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam(jo, QStringLiteral("rooms"), pod.rooms); addParam(jo, QStringLiteral("contains_url"), pod.containsUrl); - addParam(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); - addParam(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); return jo; } @@ -26,10 +24,6 @@ RoomEventFilter FromJsonObject::operator()(const QJsonObject& j fromJson(jo.value("rooms"_ls)); result.containsUrl = fromJson(jo.value("contains_url"_ls)); - result.lazyLoadMembers = - fromJson(jo.value("lazy_load_members"_ls)); - result.includeRedundantMembers = - fromJson(jo.value("include_redundant_members"_ls)); return result; } diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 87f01189..00f1e1fb 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -19,12 +19,8 @@ namespace QMatrixClient QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are included. QStringList rooms; - /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``. - bool containsUrl; - /// If ``true``, the only ``m.room.member`` events returned in the ``state`` section of the ``/sync`` response are those which are definitely necessary for a client to display the ``sender`` of the timeline events in that response. If ``false``, ``m.room.member`` events are not filtered. By default, servers should suppress duplicate redundant lazy-loaded ``m.room.member`` events from being sent to a given client across multiple calls to ``/sync``, given that most clients cache membership events (see include_redundant_members to change this behaviour). - bool lazyLoadMembers; - /// If ``true``, the ``state`` section of the ``/sync`` response will always contain the ``m.room.member`` events required to display the ``sender`` of the timeline events in that response, assuming ``lazy_load_members`` is enabled. This means that redundant duplicate member events may be returned across multiple calls to ``/sync``. This is useful for naive clients who never track membership data. If ``false``, duplicate ``m.room.member`` events may be suppressed by the server across multiple calls to ``/sync``. If ``lazy_load_members`` is ``false`` this field is ignored. - bool includeRedundantMembers; + /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. If omitted, ``url`` key is not considered for filtering. + Omittable containsUrl; }; QJsonObject toJson(const RoomEventFilter& pod); diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index ca275a9a..592038dc 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -24,7 +24,7 @@ namespace QMatrixClient /// The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. Omittable ephemeral; /// Include rooms that the user has left in the sync, default false - bool includeLeave; + Omittable includeLeave; /// The state events to include for rooms. Omittable state; /// The message and state update events to include for rooms. diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 137afbfc..52c8ea42 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -59,7 +59,7 @@ namespace QMatrixClient // Result properties - /// The joined room id + /// The joined room ID. const QString& roomId() const; protected: @@ -138,7 +138,7 @@ namespace QMatrixClient // Result properties - /// The joined room id + /// The joined room ID. const QString& roomId() const; protected: diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 2fdb2005..4e3661e1 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -131,7 +131,7 @@ BaseJob::Query queryToQueryPublicRooms(const QString& server) static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob"); -QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable limit, const QString& since, const Omittable& filter, bool includeAllNetworks, const QString& thirdPartyInstanceId) +QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable limit, const QString& since, const Omittable& filter, Omittable includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QueryPublicRoomsJobName, basePath % "/publicRooms", queryToQueryPublicRooms(server)) diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 8401c134..a6498745 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -156,7 +156,7 @@ namespace QMatrixClient * The specific third party network/protocol to request from the * homeserver. Can only be used if ``include_all_networks`` is false. */ - explicit QueryPublicRoomsJob(const QString& server = {}, Omittable limit = none, const QString& since = {}, const Omittable& filter = none, bool includeAllNetworks = false, const QString& thirdPartyInstanceId = {}); + explicit QueryPublicRoomsJob(const QString& server = {}, Omittable limit = none, const QString& since = {}, const Omittable& filter = none, Omittable includeAllNetworks = none, const QString& thirdPartyInstanceId = {}); ~QueryPublicRoomsJob() override; // Result properties diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 7aba8b61..460e2a76 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -30,7 +30,7 @@ class GetPresenceJob::Private QString presence; Omittable lastActiveAgo; QString statusMsg; - bool currentlyActive; + Omittable currentlyActive; }; QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) @@ -65,7 +65,7 @@ const QString& GetPresenceJob::statusMsg() const return d->statusMsg; } -bool GetPresenceJob::currentlyActive() const +Omittable GetPresenceJob::currentlyActive() const { return d->currentlyActive; } diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 86b9d395..c8f80357 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -65,7 +65,7 @@ namespace QMatrixClient /// The state message for this user if one was set. const QString& statusMsg() const; /// Whether the user is currently active - bool currentlyActive() const; + Omittable currentlyActive() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index d20db88a..0ca13368 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -107,7 +107,7 @@ namespace QMatrixClient static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); -PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, bool append) +PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, Omittable append) : BaseJob(HttpVerb::Post, PostPusherJobName, basePath % "/pushers/set") { diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 2b506183..da3303fe 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -164,6 +164,6 @@ namespace QMatrixClient * other pushers with the same App ID and pushkey for different * users. The default is ``false``. */ - explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, bool append = false); + explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, Omittable append = none); }; } // namespace QMatrixClient diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 320ec796..34c34861 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -30,7 +30,7 @@ BaseJob::Query queryToRegister(const QString& kind) static const auto RegisterJobName = QStringLiteral("RegisterJob"); -RegisterJob::RegisterJob(const QString& kind, const Omittable& auth, bool bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, bool inhibitLogin) +RegisterJob::RegisterJob(const QString& kind, const Omittable& auth, Omittable bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, Omittable inhibitLogin) : BaseJob(HttpVerb::Post, RegisterJobName, basePath % "/register", queryToRegister(kind), @@ -251,7 +251,7 @@ DeactivateAccountJob::DeactivateAccountJob(const Omittable& class CheckUsernameAvailabilityJob::Private { public: - bool available; + Omittable available; }; BaseJob::Query queryToCheckUsernameAvailability(const QString& username) @@ -281,7 +281,7 @@ CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& userna CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default; -bool CheckUsernameAvailabilityJob::available() const +Omittable CheckUsernameAvailabilityJob::available() const { return d->available; } diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 9002b5c8..ca1a1c21 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -80,7 +80,7 @@ namespace QMatrixClient * returned from this call, therefore preventing an automatic * login. Defaults to false. */ - explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable& auth = none, bool bindEmail = false, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, bool inhibitLogin = false); + explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable& auth = none, Omittable bindEmail = none, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, Omittable inhibitLogin = none); ~RegisterJob() override; // Result properties @@ -418,7 +418,7 @@ namespace QMatrixClient /// A flag to indicate that the username is available. This should always /// be ``true`` when the server replies with 200 OK. - bool available() const; + Omittable available() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 85b0886b..86a0ee92 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -39,7 +39,7 @@ namespace QMatrixClient /// historic profile information for the users /// that sent the events that were returned. /// By default, this is ``false``. - bool includeProfile; + Omittable includeProfile; }; /// Configuration for group. @@ -74,7 +74,7 @@ namespace QMatrixClient Omittable eventContext; /// Requests the server return the current state for /// each room returned. - bool includeState; + Omittable includeState; /// Requests that the server partitions the result set /// based on the provided list of keys. Omittable groupings; -- cgit v1.2.3 From 07c9eacc0d0009040e359fd822674a48ef8edeac Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 13 Dec 2018 07:59:45 +0900 Subject: Bump room state cache version to reset the cache --- lib/syncdata.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/syncdata.h b/lib/syncdata.h index 663553bc..8694626e 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -100,7 +100,7 @@ namespace QMatrixClient { QStringList unresolvedRooms() const { return unresolvedRoomIds; } - static std::pair cacheVersion() { return { 9, 0 }; } + static std::pair cacheVersion() { return { 10, 0 }; } static QString fileNameForRoom(QString roomId); private: -- cgit v1.2.3 From cb5f0f61e74cdc4ee64530cd73af0d080538bc1e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 13 Dec 2018 10:17:02 +0900 Subject: Connection: initialize lazyLoading member variable --- lib/connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 76e61ed1..a16bc753 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -94,7 +94,7 @@ class Connection::Private bool cacheState = true; bool cacheToBinary = SettingsGroup("libqmatrixclient") .value("cache_type").toString() != "json"; - bool lazyLoading; + bool lazyLoading = false; void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); -- cgit v1.2.3 From cf4759edba82baf51dd40285d2e13b200ca7fd29 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 13 Dec 2018 15:54:02 +0900 Subject: qmc-example: Fix the lazy-loading test --- examples/qmc-example.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index bdb9ef3f..e66687da 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -83,6 +83,8 @@ void QMCTest::setup(const QString& testRoomName) // Setting up sync loop c->setLazyLoading(true); c->sync(); + connectSingleShot(c.data(), &Connection::syncDone, + this, &QMCTest::startTests); connect(c.data(), &Connection::syncDone, c.data(), [this,testRoomName] { cout << "Sync complete, " << running.size() << " tests in the air" << endl; @@ -116,7 +118,6 @@ void QMCTest::setup(const QString& testRoomName) targetRoom = room; QMC_CHECK("Join room", true); - startTests(); }); } } @@ -167,15 +168,16 @@ void QMCTest::loadMembers() // It's not exactly correct because an arbitrary server might not support // lazy loading; but in the absence of capabilities framework we assume // it does. - if (r->memberNames().size() < r->joinedCount()) + if (r->memberNames().size() >= r->joinedCount()) { cout << "Lazy loading doesn't seem to be enabled" << endl; QMC_CHECK("Loading members", false); return; } r->setDisplayed(); - connect(r, &Room::allMembersLoaded, [this] { - QMC_CHECK("Loading members", true); + connect(r, &Room::allMembersLoaded, [this,r] { + QMC_CHECK("Loading members", + r->memberNames().size() + 1 >= r->joinedCount()); }); } -- cgit v1.2.3 From 2cbb053faeae1f23606c56ef9fd9d13ca4a2dd21 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 13 Dec 2018 19:56:18 +0900 Subject: Room::getAllMembers: fix off-by-one error --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 7232741a..8f9095dd 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -595,7 +595,7 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. - if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) + if (q->joinedCount() - 1 <= membersMap.size() || isJobRunning(allMembersJob)) return; allMembersJob = connection->callApi( -- cgit v1.2.3 From 12a0b95fdcfea15cd0ef313aec8868656629b986 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 14 Dec 2018 12:33:04 +0900 Subject: qmc-example: clearer QMC_CHECK; start tests only after the first sync is done Because lazy-loading test is executed on a room different from the test room. --- examples/qmc-example.cpp | 93 ++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index e66687da..48787e44 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -20,10 +20,10 @@ using namespace std::placeholders; class QMCTest : public QObject { public: - QMCTest(Connection* conn, const QString& testRoomName, QString source); + QMCTest(Connection* conn, QString testRoomName, QString source); private slots: - void setup(const QString& testRoomName); + void setup(); void onNewRoom(Room* r); void startTests(); void loadMembers(); @@ -44,37 +44,45 @@ class QMCTest : public QObject QStringList succeeded; QStringList failed; QString origin; + QString testRoomName; Room* targetRoom = nullptr; }; #define QMC_CHECK(description, condition) \ { \ - const bool result = !!(condition); \ Q_ASSERT(running.removeOne(description)); \ - (result ? succeeded : failed).push_back(description); \ - cout << (description) << (result ? " successul" : " FAILED") << endl; \ - if (targetRoom) \ - targetRoom->postMessage(origin % ": " % QStringLiteral(description) % \ - (result ? QStringLiteral(" successful") : QStringLiteral(" FAILED")), \ - result ? MessageEventType::Notice : MessageEventType::Text); \ + if (!!(condition)) \ + { \ + succeeded.push_back(description); \ + cout << (description) << " successful" << endl; \ + if (targetRoom) \ + targetRoom->postMessage( \ + origin % ": " % (description) % " successful", \ + MessageEventType::Notice); \ + } else { \ + failed.push_back(description); \ + cout << (description) << " FAILED" << endl; \ + if (targetRoom) \ + targetRoom->postPlainText( \ + origin % ": " % (description) % " FAILED"); \ + } \ } -QMCTest::QMCTest(Connection* conn, const QString& testRoomName, QString source) - : c(conn), origin(std::move(source)) +QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) + : c(conn), origin(std::move(source)), testRoomName(std::move(testRoomName)) { if (!origin.isEmpty()) cout << "Origin for the test message: " << origin.toStdString() << endl; if (!testRoomName.isEmpty()) cout << "Test room name: " << testRoomName.toStdString() << endl; - connect(c.data(), &Connection::connected, - this, std::bind(&QMCTest::setup, this, testRoomName)); + connect(c.data(), &Connection::connected, this, &QMCTest::setup); connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); // Big countdown watchdog QTimer::singleShot(180000, this, &QMCTest::leave); } -void QMCTest::setup(const QString& testRoomName) +void QMCTest::setup() { cout << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; @@ -85,7 +93,7 @@ void QMCTest::setup(const QString& testRoomName) c->sync(); connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::startTests); - connect(c.data(), &Connection::syncDone, c.data(), [this,testRoomName] { + connect(c.data(), &Connection::syncDone, c.data(), [this] { cout << "Sync complete, " << running.size() << " tests in the air" << endl; if (!running.isEmpty()) @@ -98,28 +106,6 @@ void QMCTest::setup(const QString& testRoomName) else finalize(); }); - - // Join a testroom, if provided - if (!targetRoom && !testRoomName.isEmpty()) - { - cout << "Joining " << testRoomName.toStdString() << endl; - running.push_back("Join room"); - auto joinJob = c->joinRoom(testRoomName); - connect(joinJob, &BaseJob::failure, this, - [this] { QMC_CHECK("Join room", false); finalize(); }); - // As of BaseJob::success, a Room object is not guaranteed to even - // exist; it's a mere confirmation that the server processed - // the request. - connect(c.data(), &Connection::loadedRoomState, this, - [this,testRoomName] (Room* room) { - Q_ASSERT(room); // It's a grave failure if room is nullptr here - if (room->canonicalAlias() != testRoomName) - return; // Not our room - - targetRoom = room; - QMC_CHECK("Join room", true); - }); - } } void QMCTest::onNewRoom(Room* r) @@ -144,12 +130,33 @@ void QMCTest::onNewRoom(Room* r) void QMCTest::startTests() { - cout << "Starting tests" << endl; - loadMembers(); - sendMessage(); - addAndRemoveTag(); - sendAndRedact(); - markDirectChat(); + if (testRoomName.isEmpty()) + return; + + cout << "Joining " << testRoomName.toStdString() << endl; + running.push_back("Join room"); + auto joinJob = c->joinRoom(testRoomName); + connect(joinJob, &BaseJob::failure, this, + [this] { QMC_CHECK("Join room", false); finalize(); }); + // As of BaseJob::success, a Room object is not guaranteed to even + // exist; it's a mere confirmation that the server processed + // the request. + connect(c.data(), &Connection::loadedRoomState, this, + [this] (Room* room) { + Q_ASSERT(room); // It's a grave failure if room is nullptr here + if (room->canonicalAlias() != testRoomName) + return; // Not our room + + targetRoom = room; + QMC_CHECK("Join room", true); + cout << "Starting tests" << endl; + + loadMembers(); + sendMessage(); + addAndRemoveTag(); + sendAndRedact(); + markDirectChat(); + }); } void QMCTest::loadMembers() -- cgit v1.2.3 From bf5401753432533b31e7d18519c2031c84e774b7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 16 Dec 2018 14:10:05 +0900 Subject: Room::getAllMembers: revert off-by-one "bugfix" It actually introduces an off-by-one error; the original code was correct. #qmatrixclient:matrix.org is used instead of #test:matrix.org to check lazy-loading (see https://github.com/matrix-org/synapse/issues/4300) --- examples/qmc-example.cpp | 6 +++--- lib/room.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 48787e44..5c05d44b 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -163,9 +163,9 @@ void QMCTest::loadMembers() { running.push_back("Loading members"); // The dedicated qmc-test room is too small to test - // lazy-loading-then-full-loading; use #test:matrix.org instead. + // lazy-loading-then-full-loading; use #qmatrixclient:matrix.org instead. // TODO: #264 - auto* r = c->room(QStringLiteral("!vfFxDRtZSSdspfTSEr:matrix.org")); + auto* r = c->room(QStringLiteral("!PCzUtxtOjUySxSelof:matrix.org")); if (!r) { cout << "#test:matrix.org is not found in the test user's rooms" << endl; @@ -184,7 +184,7 @@ void QMCTest::loadMembers() r->setDisplayed(); connect(r, &Room::allMembersLoaded, [this,r] { QMC_CHECK("Loading members", - r->memberNames().size() + 1 >= r->joinedCount()); + r->memberNames().size() >= r->joinedCount()); }); } diff --git a/lib/room.cpp b/lib/room.cpp index 8f9095dd..7232741a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -595,7 +595,7 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. - if (q->joinedCount() - 1 <= membersMap.size() || isJobRunning(allMembersJob)) + if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) return; allMembersJob = connection->callApi( -- cgit v1.2.3 From 6276e6694a8fe2f8b37374ac8080a92721064eba Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 16 Dec 2018 14:11:51 +0900 Subject: Room: messageSent(), better pendingEventAboutToAdd(), more doc-comments --- lib/room.cpp | 3 ++- lib/room.h | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 7232741a..156b5b1f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1260,7 +1260,7 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); - emit q->pendingEventAboutToAdd(); + emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); return doSendEvent(pEvent); @@ -1290,6 +1290,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) this, pEvent, txnId, call)); Room::connect(call, &BaseJob::success, q, [this,call,pEvent,txnId] { + emit q->messageSent(txnId, call->eventId()); // 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); diff --git a/lib/room.h b/lib/room.h index f7eb224e..6384b706 100644 --- a/lib/room.h +++ b/lib/room.h @@ -407,14 +407,35 @@ namespace QMatrixClient void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); - void pendingEventAboutToAdd(); + /// The event is about to be appended to the list of pending events + void pendingEventAboutToAdd(RoomEvent* event); + /// An event has been appended to the list of pending events void pendingEventAdded(); + /// The remote echo has arrived with the sync and will be merged + /// with its local counterpart + /** NB: Requires a sync loop to be emitted */ void pendingEventAboutToMerge(RoomEvent* serverEvent, int pendingEventIndex); + /// The remote and local copies of the event have been merged + /** NB: Requires a sync loop to be emitted */ void pendingEventMerged(); + /// An event will be removed from the list of pending events void pendingEventAboutToDiscard(int pendingEventIndex); + /// An event has just been removed from the list of pending events void pendingEventDiscarded(); + /// The status of a pending event has changed + /** \sa PendingEventItem::deliveryStatus */ void pendingEventChanged(int pendingEventIndex); + /// The server accepted the message + /** This is emitted when an event sending request has successfully + * completed. This does not mean that the event is already in the + * local timeline, only that the server has accepted it. + * \param txnId transaction id assigned by the client during sending + * \param eventId event id assigned by the server upon acceptance + * \sa postEvent, postPlainText, postMessage, postHtmlMessage + * \sa pendingEventMerged, aboutToAddNewMessages + */ + void messageSent(QString txnId, QString eventId); /** A common signal for various kinds of changes in the room * Aside from all changes in the room state -- cgit v1.2.3 From 2b68f4ae10d6c61b47c983e496380c86da1ff211 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 16 Dec 2018 14:19:15 +0900 Subject: qmc-example: refactor QMCTest to properly order actions To be more specific: - No race condition in running tests when the test room is already joined; joining occurs before the very first sync. - qmc-example doesn't (in vain) wait for the last sync in order to make sure the final message ("All tests finished") is delivered - uses Room::messageSent() instead now. - Running QMCTest::loadMembers() does not rely on having a test room --- examples/qmc-example.cpp | 111 +++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 5c05d44b..a5e5a481 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -20,12 +20,13 @@ using namespace std::placeholders; class QMCTest : public QObject { public: - QMCTest(Connection* conn, QString testRoomName, QString source); + QMCTest(Connection* conn, QString targetRoomName, QString source); private slots: - void setup(); + void setupAndRun(); void onNewRoom(Room* r); - void startTests(); + void run(); + void doTests(); void loadMembers(); void sendMessage(); void addAndRemoveTag(); @@ -44,7 +45,7 @@ class QMCTest : public QObject QStringList succeeded; QStringList failed; QString origin; - QString testRoomName; + QString targetRoomName; Room* targetRoom = nullptr; }; @@ -69,43 +70,42 @@ class QMCTest : public QObject } QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) - : c(conn), origin(std::move(source)), testRoomName(std::move(testRoomName)) + : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName)) { if (!origin.isEmpty()) cout << "Origin for the test message: " << origin.toStdString() << endl; - if (!testRoomName.isEmpty()) - cout << "Test room name: " << testRoomName.toStdString() << endl; + if (!targetRoomName.isEmpty()) + cout << "Test room name: " << targetRoomName.toStdString() << endl; - connect(c.data(), &Connection::connected, this, &QMCTest::setup); + connect(c.data(), &Connection::connected, this, &QMCTest::setupAndRun); connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); // Big countdown watchdog QTimer::singleShot(180000, this, &QMCTest::leave); } -void QMCTest::setup() +void QMCTest::setupAndRun() { cout << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; cout << "Access token: " << c->accessToken().toStdString() << endl; - // Setting up sync loop - c->setLazyLoading(true); - c->sync(); - connectSingleShot(c.data(), &Connection::syncDone, - this, &QMCTest::startTests); - connect(c.data(), &Connection::syncDone, c.data(), [this] { - cout << "Sync complete, " - << running.size() << " tests in the air" << endl; - if (!running.isEmpty()) - c->sync(10000); - else if (targetRoom) - { - targetRoom->postPlainText(origin % ": All tests finished"); - connect(targetRoom, &Room::pendingEventMerged, this, &QMCTest::leave); - } - else - finalize(); - }); + if (!targetRoomName.isEmpty()) + { + cout << "Joining " << targetRoomName.toStdString() << endl; + running.push_back("Join room"); + auto joinJob = c->joinRoom(targetRoomName); + connect(joinJob, &BaseJob::failure, this, + [this] { QMC_CHECK("Join room", false); finalize(); }); + // Connection::joinRoom() creates a Room object upon JoinRoomJob::success + // but this object is empty until the first sync is done. + connect(joinJob, &BaseJob::success, this, [this,joinJob] { + targetRoom = c->room(joinJob->roomId(), JoinState::Join); + QMC_CHECK("Join room", targetRoom != nullptr); + + run(); + }); + } else + run(); } void QMCTest::onNewRoom(Room* r) @@ -128,35 +128,40 @@ void QMCTest::onNewRoom(Room* r) }); } -void QMCTest::startTests() +void QMCTest::run() { - if (testRoomName.isEmpty()) + c->setLazyLoading(true); + c->sync(); + connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests); + connect(c.data(), &Connection::syncDone, c.data(), [this] { + cout << "Sync complete, " + << running.size() << " tests in the air" << endl; + if (!running.isEmpty()) + c->sync(10000); + else if (targetRoom) + { + targetRoom->postPlainText(origin % ": All tests finished"); + connect(targetRoom, &Room::messageSent, this, &QMCTest::leave); + } + else + finalize(); + }); +} + +void QMCTest::doTests() +{ + cout << "Starting tests" << endl; + + loadMembers(); + // Add here tests not requiring the test room + if (targetRoomName.isEmpty()) return; - cout << "Joining " << testRoomName.toStdString() << endl; - running.push_back("Join room"); - auto joinJob = c->joinRoom(testRoomName); - connect(joinJob, &BaseJob::failure, this, - [this] { QMC_CHECK("Join room", false); finalize(); }); - // As of BaseJob::success, a Room object is not guaranteed to even - // exist; it's a mere confirmation that the server processed - // the request. - connect(c.data(), &Connection::loadedRoomState, this, - [this] (Room* room) { - Q_ASSERT(room); // It's a grave failure if room is nullptr here - if (room->canonicalAlias() != testRoomName) - return; // Not our room - - targetRoom = room; - QMC_CHECK("Join room", true); - cout << "Starting tests" << endl; - - loadMembers(); - sendMessage(); - addAndRemoveTag(); - sendAndRedact(); - markDirectChat(); - }); + sendMessage(); + addAndRemoveTag(); + sendAndRedact(); + markDirectChat(); + // Add here tests with the test room } void QMCTest::loadMembers() -- cgit v1.2.3 From d81e33841e0e2bb7dacc562aea7b820900b0d074 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 16 Dec 2018 14:27:04 +0900 Subject: qmc-example: check Room::messageSent() more carefully --- examples/qmc-example.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index a5e5a481..652c1f92 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -140,8 +140,16 @@ void QMCTest::run() c->sync(10000); else if (targetRoom) { - targetRoom->postPlainText(origin % ": All tests finished"); - connect(targetRoom, &Room::messageSent, this, &QMCTest::leave); + // TODO: Waiting for proper futures to come so that it could be: +// targetRoom->postPlainText(origin % ": All tests finished") +// .then(this, &QMCTest::leave); + auto txnId = + targetRoom->postPlainText(origin % ": All tests finished"); + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (QString serverTxnId) { + if (txnId == serverTxnId) + leave(); + }); } else finalize(); -- cgit v1.2.3 From 163f0acb78b62eeb5ca75ead2fa0beba816fb68a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 16 Dec 2018 17:00:15 +0000 Subject: fix macOS installation instructions --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cbb8d5f0..72eaf7ab 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrix Tags starting with `v` represent released versions; `rc` mark release candidates. ### Pre-requisites -- a Linux, OSX or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) +- a Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) - For Ubuntu flavours - zesty or later (or a derivative) is good enough out of the box; older ones will need PPAs at least for a newer Qt; in particular, if you have xenial you're advised to add Kubuntu Backports PPA for it - a Git client to check out this repo - Qt 5 (either Open Source or Commercial), version 5.6 or higher @@ -29,18 +29,18 @@ Tags starting with `v` represent released versions; `rc` mark release candidates - CMake (from your package management system or [the official website](https://cmake.org/download/)) - or qmake (comes with Qt) - a C++ toolchain supported by your version of Qt (see a link for your platform at [the Qt's platform requirements page](http://doc.qt.io/qt-5/gettingstarted.html#platform-requirements)) - - GCC 5 (Windows, Linux, OSX), Clang 5 (Linux), Apple Clang 8.1 (OSX) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted + - GCC 5 (Windows, Linux, macOS), Clang 5 (Linux), Apple Clang 8.1 (macOS) and Visual C++ 2015 (Windows) are the oldest officially supported; Clang 3.8 and GCC 4.9.2 are known to still work, maintenance patches for them are accepted - any build system that works with CMake and/or qmake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. #### Linux Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails. -#### OS X -`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you may need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=` +#### macOS +`brew install qt5` should get you a recent Qt5. If you plan to use CMake, you will need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` #### Windows 1. Install Qt5, using their official installer. -1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for OS X (see above), also helps. +1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for macOS (see above), also helps. There are no official MinGW-based 64-bit packages for Qt. If you're determined to build a 64-bit library, either use a Visual Studio toolchain or build Qt5 yourself as described in Qt documentation. @@ -53,7 +53,7 @@ cd build_dir cmake .. # Pass -DCMAKE_PREFIX_PATH and -DCMAKE_INSTALL_PREFIX here if needed cmake --build . --target all ``` -This will get you the compiled library in `build_dir` inside your project sources. Static builds are tested on all supported platforms. Dynamic builds of libqmatrixclient are only tested on Linux at the moment; experiments with dynamic builds on Windows/OSX are welcome. Taking a look at [qmc-example](https://github.com/QMatrixClient/libqmatrixclient/tree/master/examples) (used to test the library) should give you a basic idea of using libQMatrixClient; for more extensive usage check out the source code of [Quaternion](https://github.com/QMatrixClient/Quaternion) (the reference client built on QMatrixClient). +This will get you the compiled library in `build_dir` inside your project sources. Static builds are tested on all supported platforms. Dynamic builds of libqmatrixclient are only tested on Linux at the moment; experiments with dynamic builds on Windows/macOS are welcome. Taking a look at [qmc-example](https://github.com/QMatrixClient/libqmatrixclient/tree/master/examples) (used to test the library) should give you a basic idea of using libQMatrixClient; for more extensive usage check out the source code of [Quaternion](https://github.com/QMatrixClient/Quaternion) (the reference client built on QMatrixClient). You can install the library with CMake: ``` -- cgit v1.2.3 From 40979a2c81f28ccd165982bd137c25a94f34c4cc Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 24 Dec 2018 13:50:02 +0900 Subject: Clarify doc-comment for BaseJob::finished a bit [skip ci] --- lib/jobs/basejob.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 3d50344d..dd6f9fc8 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -215,9 +215,9 @@ namespace QMatrixClient * * In general, to be notified of a job's completion, client code * should connect to result(), success(), or failure() - * rather than finished(). However if you store a list of jobs - * and need to track their lifecycle, then you should connect to this - * instead of result(), to avoid dangling pointers in your list. + * rather than finished(). However if you need to track the job's + * lifecycle you should connect to this instead of result(); + * in particular, only this signal will be emitted on abandoning. * * @param job the job that emitted this signal * -- cgit v1.2.3 From 5e85dba348676009b2e3b1e41ce9d3e7b8bca1ca Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 26 Dec 2018 10:56:49 +0900 Subject: RoomAvatarEvent: use correct #includes --- lib/events/roomavatarevent.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 491861b1..a43d3a85 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -18,8 +18,7 @@ #pragma once -#include "event.h" - +#include "stateevent.h" #include "eventcontent.h" namespace QMatrixClient -- cgit v1.2.3 From 241f7165816624da45fca58578885b17589ec1f7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 26 Dec 2018 10:57:50 +0900 Subject: EventContent: allow empty (default-constructed) thumbnails --- lib/events/eventcontent.h | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index bedf0078..ea321fb6 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -149,10 +149,10 @@ namespace QMatrixClient class Thumbnail : public ImageInfo { public: + Thumbnail() : ImageInfo(QUrl()) { } // To allow empty thumbnails Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) - : ImageInfo(info) - { } + Thumbnail(const ImageInfo& info) : ImageInfo(info) { } + using ImageInfo::ImageInfo; /** * Writes thumbnail information to "thumbnail_info" subobject @@ -184,9 +184,7 @@ namespace QMatrixClient class UrlBasedContent : public TypedBase, public InfoT { public: - UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) - : InfoT(url, std::forward(info), filename) - { } + using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(json["url"].toString(), json["info"].toObject(), @@ -214,7 +212,7 @@ namespace QMatrixClient class UrlWithThumbnailContent : public UrlBasedContent { public: - // TODO: POD constructor + using UrlBasedContent::UrlBasedContent; explicit UrlWithThumbnailContent(const QJsonObject& json) : UrlBasedContent(json) , thumbnail(InfoT::originalInfoJson) -- cgit v1.2.3 From eb8a2396c4e0a5e4e38b743d14891a0d51a43e41 Mon Sep 17 00:00:00 2001 From: qso Date: Fri, 4 Jan 2019 23:43:48 +0100 Subject: added option for installation of qmc-example application --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a3193a4..2881fe7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.1) project(qmatrixclient CXX) +option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) + include(CheckCXXCompilerFlag) if (NOT WIN32) include(GNUInstallDirs) @@ -198,7 +200,9 @@ if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) -install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +if (QMATRIXCLIENT_INSTALL_EXAMPLE) + install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif (QMATRIXCLIENT_INSTALL_EXAMPLE) if (UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) -- cgit v1.2.3 From 6a34116431067e8f5d56b8e24d6205ed1b9e35c8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 16:48:36 +0900 Subject: qmc-example: streamline redaction test ...using features from the new lib. --- examples/qmc-example.cpp | 76 +++++++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 652c1f92..894167a9 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -32,7 +32,7 @@ class QMCTest : public QObject void addAndRemoveTag(); void sendAndRedact(); void checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events); + const QMetaObject::Connection& sc); void markDirectChat(); void checkDirectChatOutcome( const Connection::DirectChatsMap& added); @@ -250,70 +250,54 @@ void QMCTest::sendAndRedact() { running.push_back("Redaction"); cout << "Sending a message to redact" << endl; - if (auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), - RoomMessageEvent(origin % ": message to redact"))) + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) { - connect(job, &BaseJob::success, targetRoom, [job,this] { + QMC_CHECK("Redaction", false); + return; + } + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (const QString& tId, const QString& evtId) { + if (tId != txnId) + return; + cout << "Redacting the message" << endl; - targetRoom->redactEvent(job->eventId(), origin); - // Make sure to save the event id because the job is about to end. - connect(targetRoom, &Room::aboutToAddNewMessages, this, - std::bind(&QMCTest::checkRedactionOutcome, - this, job->eventId(), _1)); + targetRoom->redactEvent(evtId, origin); + QMetaObject::Connection sc; + sc = connect(targetRoom, &Room::addedMessages, this, + [this,sc,evtId] { checkRedactionOutcome(evtId, sc); }); }); - } else - QMC_CHECK("Redaction", false); } void QMCTest::checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events) + const QMetaObject::Connection& sc) { - static bool checkSucceeded = false; // There are two possible (correct) outcomes: either the event comes already // redacted at the next sync, or the nearest sync completes with // the unredacted event but the next one brings redaction. - auto it = std::find_if(events.begin(), events.end(), - [=] (const RoomEventPtr& e) { - return e->id() == evtIdToRedact; - }); - if (it == events.end()) + auto it = targetRoom->findInTimeline(evtIdToRedact); + if (it == targetRoom->timelineEdge()) return; // Waiting for the next sync if ((*it)->isRedacted()) { - if (checkSucceeded) - { - const auto msg = - "The redacted event came in with the sync again, ignoring"; - cout << msg << endl; - targetRoom->postPlainText(msg); - return; - } cout << "The sync brought already redacted message" << endl; QMC_CHECK("Redaction", true); - // Not disconnecting because there are other connections from this class - // to aboutToAddNewMessages - checkSucceeded = true; + disconnect(sc); return; } - // The event is not redacted - if (checkSucceeded) - { - const auto msg = - "Warning: the redacted event came non-redacted with the sync!"; - cout << msg << endl; - targetRoom->postPlainText(msg); - } - cout << "Message came non-redacted with the sync, waiting for redaction" << endl; - connect(targetRoom, &Room::replacedEvent, targetRoom, - [=] (const RoomEvent* newEvent, const RoomEvent* oldEvent) { - QMC_CHECK("Redaction", oldEvent->id() == evtIdToRedact && - newEvent->isRedacted() && - newEvent->redactionReason() == origin); - checkSucceeded = true; - disconnect(targetRoom, &Room::replacedEvent, nullptr, nullptr); + cout << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connect(targetRoom, &Room::replacedEvent, this, + [this,evtIdToRedact] + (const RoomEvent* newEvent, const RoomEvent* oldEvent) { + if (oldEvent->id() == evtIdToRedact) + { + QMC_CHECK("Redaction", newEvent->isRedacted() && + newEvent->redactionReason() == origin); + disconnect(targetRoom, &Room::replacedEvent, nullptr, nullptr); + } }); - } void QMCTest::markDirectChat() -- cgit v1.2.3 From f545d181ade8736dfda93e8abb34ab93ac34e931 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 18:55:29 +0900 Subject: .travis.yml: use Homebrew addon and newer image The current default lands on a blunder with SSLSetALPNProtocols function (similar to https://stackoverflow.com/questions/46685756/how-do-i-make- use-of-sslsetalpnprotocols). --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0b2967cf..b515b8fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ addons: - g++-5 - qt57base - valgrind + homebrew: + packages: + - qt5 matrix: include: @@ -18,7 +21,8 @@ matrix: - os: linux compiler: clang - os: osx - env: [ 'ENV_EVAL="brew update && brew install qt5 && PATH=/usr/local/opt/qt/bin:$PATH"' ] + osx_image: xcode10 + env: [ 'ENV_EVAL="PATH=/usr/local/opt/qt/bin:$PATH"' ] before_install: - eval "${ENV_EVAL}" -- cgit v1.2.3 From 3b88c2b537b6cb98dcd0f2066d39e426b5cc52da Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 26 Dec 2018 19:34:15 +0900 Subject: Connection::upload*: autodetect content type if not supplied --- lib/connection.cpp | 15 ++++++++++++--- lib/connection.h | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a16bc753..c17cbffc 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include using namespace QMatrixClient; @@ -466,13 +467,21 @@ MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, } UploadContentJob* Connection::uploadContent(QIODevice* contentSource, - const QString& filename, const QString& contentType) const + const QString& filename, const QString& overrideContentType) const { + auto contentType = overrideContentType; + if (contentType.isEmpty()) + { + contentType = + QMimeDatabase().mimeTypeForFileNameAndData(filename, contentSource) + .name(); + contentSource->open(QIODevice::ReadOnly); + } return callApi(contentSource, filename, contentType); } UploadContentJob* Connection::uploadFile(const QString& fileName, - const QString& contentType) + const QString& overrideContentType) { auto sourceFile = new QFile(fileName); if (!sourceFile->open(QIODevice::ReadOnly)) @@ -482,7 +491,7 @@ UploadContentJob* Connection::uploadFile(const QString& fileName, return nullptr; } return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), - contentType); + overrideContentType); } GetContentJob* Connection::getContent(const QString& mediaId) const diff --git a/lib/connection.h b/lib/connection.h index 9a94aad6..ff3e2028 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -402,10 +402,10 @@ namespace QMatrixClient // QIODevice* should already be open UploadContentJob* uploadContent(QIODevice* contentSource, - const QString& filename = {}, - const QString& contentType = {}) const; + const QString& filename = {}, + const QString& overrideContentType = {}) const; UploadContentJob* uploadFile(const QString& fileName, - const QString& contentType = {}); + const QString& overrideContentType = {}); GetContentJob* getContent(const QString& mediaId) const; GetContentJob* getContent(const QUrl& url) const; // If localFilename is empty, a temporary file will be created -- cgit v1.2.3 From e017dd42637071687f88f5a36e7e03f1536332be Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 12:53:01 +0900 Subject: FileTransferInfo: new properties: isUpload and started Also: use constructors instead of list-based initialisation in FileTransferPrivateInfo to enable a case of "invalid/empty" FileTransferPrivateInfo with status == None. --- lib/room.cpp | 16 +++++++++------- lib/room.h | 12 ++++++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 156b5b1f..d613fd77 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -126,15 +126,17 @@ class Room::Private struct FileTransferPrivateInfo { -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST FileTransferPrivateInfo() = default; - FileTransferPrivateInfo(BaseJob* j, QString fileName) - : job(j), localFileInfo(fileName) + FileTransferPrivateInfo(BaseJob* j, const QString& fileName, + bool isUploading = false) + : status(FileTransferInfo::Started), job(j) + , localFileInfo(fileName), isUpload(isUploading) { } -#endif + + FileTransferInfo::Status status = FileTransferInfo::None; QPointer job = nullptr; QFileInfo localFileInfo { }; - FileTransferInfo::Status status = FileTransferInfo::Started; + bool isUpload = false; qint64 progress = 0; qint64 total = -1; @@ -969,7 +971,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); return fti; #else - return { infoIt->status, int(progress), int(total), + return { infoIt->status, infoIt->isUpload, int(progress), int(total), QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) }; @@ -1532,7 +1534,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobRunning(job)) { - d->fileTransfers.insert(id, { job, fileName }); + d->fileTransfers.insert(id, { job, fileName, true }); connect(job, &BaseJob::uploadProgress, this, [this,id] (qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); diff --git a/lib/room.h b/lib/room.h index 6384b706..b832977f 100644 --- a/lib/room.h +++ b/lib/room.h @@ -42,10 +42,17 @@ namespace QMatrixClient class SetRoomStateWithKeyJob; class RedactEventJob; + /** The data structure used to expose file transfer information to views + * + * This is specifically tuned to work with QML exposing all traits as + * Q_PROPERTY values. + */ class FileTransferInfo { Q_GADGET + Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool started READ started CONSTANT) Q_PROPERTY(bool completed READ completed CONSTANT) Q_PROPERTY(bool failed READ failed CONSTANT) Q_PROPERTY(int progress MEMBER progress CONSTANT) @@ -55,14 +62,15 @@ namespace QMatrixClient public: enum Status { None, Started, Completed, Failed }; Status status = None; + bool isUpload = false; int progress = 0; int total = -1; QUrl localDir { }; QUrl localPath { }; - bool active() const - { return status == Started || status == Completed; } + bool started() const { return status == Started; } bool completed() const { return status == Completed; } + bool active() const { return started() || completed(); } bool failed() const { return status == Failed; } }; -- cgit v1.2.3 From 143fffcf3962184befbbe37bebc5544d25bc7c39 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 12:55:21 +0900 Subject: Room::fileSource Also: const'ified other methods related to file urls. --- lib/room.cpp | 21 ++++++++++++++++++--- lib/room.h | 7 ++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index d613fd77..23bbbc5b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -911,7 +911,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const return fileName; } -QUrl Room::urlToThumbnail(const QString& eventId) +QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) if (event->hasThumbnail()) @@ -925,7 +925,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) return {}; } -QUrl Room::urlToDownload(const QString& eventId) +QUrl Room::urlToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) { @@ -937,7 +937,7 @@ QUrl Room::urlToDownload(const QString& eventId) return {}; } -QString Room::fileNameToDownload(const QString& eventId) +QString Room::fileNameToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) return d->fileNameToDownload(event); @@ -978,6 +978,21 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const #endif } +QUrl Room::fileSource(const QString& id) const +{ + auto url = urlToDownload(id); + if (url.isValid()) + return url; + + // No urlToDownload means it's a pending or completed upload. + auto infoIt = d->fileTransfers.find(id); + if (infoIt != d->fileTransfers.end()) + return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + + qCWarning(MAIN) << "File source for identifier" << id << "not found"; + return {}; +} + QString Room::prettyPrint(const QString& plainText) const { return QMatrixClient::prettyPrint(plainText); diff --git a/lib/room.h b/lib/room.h index b832977f..a166be37 100644 --- a/lib/room.h +++ b/lib/room.h @@ -342,10 +342,11 @@ namespace QMatrixClient /// Get the list of users this room is a direct chat with QList directChatUsers() const; - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); - Q_INVOKABLE QUrl urlToDownload(const QString& eventId); - Q_INVOKABLE QString fileNameToDownload(const QString& eventId); + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; + Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; + Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + Q_INVOKABLE QUrl fileSource(const QString& id) const; /** Pretty-prints plain text into HTML * As of now, it's exactly the same as QMatrixClient::prettyPrint(); -- cgit v1.2.3 From 3ecf762f497a4d4b6ea7583689c0b9b284300201 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 12:47:02 +0900 Subject: EventContent: use qint64 for the payload size --- lib/events/eventcontent.cpp | 8 +++++--- lib/events/eventcontent.h | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index a6b1c763..bac2b72f 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -17,6 +17,8 @@ */ #include "eventcontent.h" + +#include "converters.h" #include "util.h" #include @@ -30,7 +32,7 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, +FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, const QString& originalFilename) : mimeType(mimeType), url(u), payloadSize(payloadSize) , originalName(originalFilename) @@ -41,7 +43,7 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, : originalInfoJson(infoJson) , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) , url(u) - , payloadSize(infoJson["size"_ls].toInt()) + , payloadSize(fromJson(infoJson["size"_ls])) , originalName(originalFilename) { if (!mimeType.isValid()) @@ -55,7 +57,7 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } -ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, +ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, const QSize& imageSize) : FileInfo(u, fileSize, mimeType), imageSize(imageSize) { } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index ea321fb6..2a48e910 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -88,7 +88,7 @@ namespace QMatrixClient class FileInfo { public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, + explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, const QMimeType& mimeType = {}, const QString& originalFilename = {}); FileInfo(const QUrl& u, const QJsonObject& infoJson, @@ -109,7 +109,7 @@ namespace QMatrixClient QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; - int payloadSize; + qint64 payloadSize; QString originalName; }; @@ -127,7 +127,7 @@ namespace QMatrixClient class ImageInfo : public FileInfo { public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, + explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, QMimeType mimeType = {}, const QSize& imageSize = {}); ImageInfo(const QUrl& u, const QJsonObject& infoJson, -- cgit v1.2.3 From 4ec3dd92d2cb5af4cf4893770e29db51d23e0e67 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 26 Dec 2018 19:31:11 +0900 Subject: Make content in events editable --- lib/events/eventcontent.h | 2 ++ lib/events/roommessageevent.cpp | 6 +++--- lib/events/roommessageevent.h | 10 ++++++++++ lib/events/stateevent.h | 6 ++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 2a48e910..2e61276b 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -167,6 +167,7 @@ namespace QMatrixClient explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } + virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } }; @@ -196,6 +197,7 @@ namespace QMatrixClient QMimeType type() const override { return InfoT::mimeType; } const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } protected: void fillJson(QJsonObject* json) const override diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 1c5cf058..572c7173 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -71,8 +71,8 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType, - TypedBase* content) +QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); json.insert(QStringLiteral("msgtype"), jsonMsgType); @@ -86,7 +86,7 @@ static const auto BodyKey = "body"_ls; RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), - toMsgJson(plainBody, jsonMsgType, content)) + assembleContentJson(plainBody, jsonMsgType, content)) , _content(content) { } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 4c29a93e..a4ba6e65 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -56,6 +56,13 @@ namespace QMatrixClient QString plainBody() const; EventContent::TypedBase* content() const { return _content.data(); } + template + void editContent(VisitorT visitor) + { + visitor(*_content); + editJson()[ContentKeyL] = + assembleContentJson(plainBody(), rawMsgtype(), content()); + } QMimeType mimeType() const; bool hasTextContent() const; bool hasFileContent() const; @@ -64,6 +71,9 @@ namespace QMatrixClient private: QScopedPointer _content; + static QJsonObject assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, EventContent::TypedBase* content); + REGISTER_ENUM(MsgType) }; REGISTER_EVENT_TYPE(RoomMessageEvent) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index d82de7e1..d488c0a0 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -88,6 +88,12 @@ namespace QMatrixClient { } const ContentT& content() const { return _content; } + template + void editContent(VisitorT&& visitor) + { + visitor(_content); + editJson()[ContentKeyL] = _content.toJson(); + } [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const { return prevContent(); } const ContentT* prevContent() const -- cgit v1.2.3 From 77830a97e524a4bd27d8cbcd3830cbe450a5755a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 12:46:13 +0900 Subject: EventContent: only dump to json non-empty/valid values --- lib/events/eventcontent.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index bac2b72f..03880885 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -53,8 +53,10 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, void FileInfo::fillInfoJson(QJsonObject* infoJson) const { Q_ASSERT(infoJson); - infoJson->insert(QStringLiteral("size"), payloadSize); - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + if (payloadSize != -1) + infoJson->insert(QStringLiteral("size"), payloadSize); + if (mimeType.isValid()) + infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, @@ -71,8 +73,10 @@ ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, void ImageInfo::fillInfoJson(QJsonObject* infoJson) const { FileInfo::fillInfoJson(infoJson); - infoJson->insert(QStringLiteral("w"), imageSize.width()); - infoJson->insert(QStringLiteral("h"), imageSize.height()); + if (imageSize.width() != -1) + infoJson->insert(QStringLiteral("w"), imageSize.width()); + if (imageSize.height() != -1) + infoJson->insert(QStringLiteral("h"), imageSize.height()); } Thumbnail::Thumbnail(const QJsonObject& infoJson) @@ -82,7 +86,9 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson) void Thumbnail::fillInfoJson(QJsonObject* infoJson) const { - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson(*this)); + if (url.isValid()) + infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); + if (!imageSize.isEmpty()) + infoJson->insert(QStringLiteral("thumbnail_info"), + toInfoJson(*this)); } -- cgit v1.2.3 From f6740316dfcdb287b019b4258df2213c31965d42 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 26 Dec 2018 19:36:00 +0900 Subject: PendingEventItem: add FileUploaded status and setFileUploaded helper function --- lib/eventitem.cpp | 26 ++++++++++++++++++++++++++ lib/eventitem.h | 15 ++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 79ef769c..8ec3fe48 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -17,3 +17,29 @@ */ #include "eventitem.h" + +#include "events/roommessageevent.h" +#include "events/roomavatarevent.h" + +using namespace QMatrixClient; + +void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) +{ + // TODO: eventually we might introduce hasFileContent to RoomEvent, + // and unify the code below. + if (auto* rme = getAs()) + { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([remoteUrl] (EventContent::TypedBase& ec) { + ec.fileInfo()->url = remoteUrl; + }); + } + if (auto* rae = getAs()) + { + Q_ASSERT(rae->content().fileInfo()); + rae->editContent([remoteUrl] (EventContent::FileInfo& fi) { + fi.url = remoteUrl; + }); + } + setStatus(EventStatus::FileUploaded); +} diff --git a/lib/eventitem.h b/lib/eventitem.h index 5f1d10c9..36ed2132 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -33,16 +33,17 @@ namespace QMatrixClient /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. - * Most status values are mutually exclusive. + * All values except Redacted and Hidden are mutually exclusive. */ enum Code { Normal = 0x0, //< No special designation Submitted = 0x01, //< The event has just been submitted for sending - Departed = 0x02, //< The event has left the client - ReachedServer = 0x03, //< The server has received the event - SendingFailed = 0x04, //< The server could not receive the event + FileUploaded = 0x02, //< The file attached to the event has been uploaded to the server + Departed = 0x03, //< The event has left the client + ReachedServer = 0x04, //< The server has received the event + SendingFailed = 0x05, //< The server could not receive the event Redacted = 0x08, //< The event has been redacted - Hidden = 0x10, //< The event should be hidden + Hidden = 0x10, //< The event should not be shown in the timeline }; Q_DECLARE_FLAGS(Status, Code) Q_FLAG(Status) @@ -70,6 +71,9 @@ namespace QMatrixClient return std::exchange(evt, move(other)); } + protected: + template + EventT* getAs() { return eventCast(evt); } private: RoomEventPtr evt; }; @@ -116,6 +120,7 @@ namespace QMatrixClient QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } + void setFileUploaded(const QUrl& remoteUrl); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); -- cgit v1.2.3 From 3dcf0d3fd1e64d64b57976e400888fe8a02c4451 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 15:00:08 +0900 Subject: Room::postFile() and supplementary things in Room::Private --- lib/room.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- lib/room.h | 1 + 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 23bbbc5b..6500366e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include @@ -67,9 +68,11 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 that fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) +// A workaround for MSVC 2015 and older GCC's that don't handle initializer +// lists right (MSVC 2015, notably, fails with "error C2440: 'return': +// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'") +#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ + (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) # define WORKAROUND_EXTENDED_INITIALIZER_LIST #endif @@ -236,6 +239,8 @@ class Room::Private return sendEvent(makeEvent(std::forward(eventArgs)...)); } + RoomEvent* addAsPending(RoomEventPtr&& event); + QString doSendEvent(const RoomEvent* pEvent); PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); void onEventSendingFailure(const RoomEvent* pEvent, @@ -1272,7 +1277,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) } } -QString Room::Private::sendEvent(RoomEventPtr&& event) +RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); @@ -1280,7 +1285,12 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); - return doSendEvent(pEvent); + return pEvent; +} + +QString Room::Private::sendEvent(RoomEventPtr&& event) +{ + return doSendEvent(addAsPending(std::move(event))); } QString Room::Private::doSendEvent(const RoomEvent* pEvent) @@ -1357,6 +1367,7 @@ QString Room::retryMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; + // TODO: Support retrying uploads it->resetStatus(); return d->doSendEvent(it->event()); } @@ -1367,6 +1378,7 @@ void Room::discardMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; + // TODO: Discard an ongoing upload if there is any emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); @@ -1394,6 +1406,42 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html, MessageEventType::Text); } +QString Room::postFile(const QString& plainText, const QUrl& localPath) +{ + QFileInfo localFile { localPath.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + // Remote URL will only be known after upload, see below. + auto* content = new EventContent::FileContent(QUrl(), localFile.size(), + QMimeDatabase().mimeTypeForUrl(localPath)); + // TODO: Set the msgtype based on MIME type + auto* pEvent = d->addAsPending( + makeEvent(plainText, "m.file", content)); + const auto txnId = pEvent->transactionId(); + uploadFile(txnId, localPath); + QMetaObject::Connection c; + c = connect(this, &Room::fileTransferCompleted, this, + [c,this,pEvent,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + if (id == txnId) + { + auto it = d->findAsPending(pEvent); + if (it != d->unsyncedEvents.end()) + { + it->setFileUploaded(mxcUri); + emit pendingEventChanged(it - d->unsyncedEvents.begin()); + d->doSendEvent(pEvent); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was cancelled"; + } + disconnect(c); + } + }); + return txnId; +} + QString Room::postEvent(RoomEvent* event) { if (usesEncryption()) diff --git a/lib/room.h b/lib/room.h index a166be37..ce1c3dd3 100644 --- a/lib/room.h +++ b/lib/room.h @@ -374,6 +374,7 @@ namespace QMatrixClient QString postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type); QString postHtmlText(const QString& plainText, const QString& html); + QString postFile(const QString& plainText, const QUrl& localPath); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one -- cgit v1.2.3 From e3c1b93483eafbb94f1224b57e562984f4100538 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 29 Dec 2018 23:12:46 +0900 Subject: Support file events in Room::retryMessage/discardMessage --- lib/room.cpp | 43 +++++++++++++++++++++++++++++++++++++++++-- lib/room.h | 2 +- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 6500366e..2c90955e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1367,7 +1367,32 @@ QString Room::retryMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; - // TODO: Support retrying uploads + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (transferIt->status == FileTransferInfo::Completed) + { + qCDebug(MAIN) << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; + } else { + if (isJobRunning(transferIt->job)) + { + qCDebug(MAIN) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload will be retried")); + } + uploadFile(txnId, + QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath())); + // FIXME: Content type is no more passed here but it should + } + } + if (it->deliveryStatus() == EventStatus::ReachedServer) + { + qCWarning(MAIN) << "The previous attempt has reached the server; two" + " events are likely to be in the timeline after retry"; + } it->resetStatus(); return d->doSendEvent(it->event()); } @@ -1378,7 +1403,21 @@ void Room::discardMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; - // TODO: Discard an ongoing upload if there is any + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (isJobRunning(transferIt->job)) + { + transferIt->status = FileTransferInfo::Cancelled; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload cancelled")); + } else if (transferIt->status == FileTransferInfo::Completed) + { + qCWarning(MAIN) << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; + } + } emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); diff --git a/lib/room.h b/lib/room.h index ce1c3dd3..2b6a2172 100644 --- a/lib/room.h +++ b/lib/room.h @@ -60,7 +60,7 @@ namespace QMatrixClient Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) public: - enum Status { None, Started, Completed, Failed }; + enum Status { None, Started, Completed, Failed, Cancelled }; Status status = None; bool isUpload = false; int progress = 0; -- cgit v1.2.3 From 816377ea3c204f22698e1458b815fdd3c3837adc Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Jan 2019 17:57:42 +0900 Subject: More defaults to construct LocationContent and PlayableContent --- lib/events/roommessageevent.cpp | 3 ++- lib/events/roommessageevent.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 572c7173..69969c0f 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -200,7 +200,8 @@ void TextContent::fillJson(QJsonObject* json) const } } -LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) +LocationContent::LocationContent(const QString& geoUri, + const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) { } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index a4ba6e65..5657135b 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -122,7 +122,7 @@ namespace QMatrixClient { public: LocationContent(const QString& geoUri, - const ImageInfo& thumbnail); + const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); QMimeType type() const override; @@ -142,6 +142,7 @@ namespace QMatrixClient class PlayableContent : public ContentT { public: + using ContentT::ContentT; PlayableContent(const QJsonObject& json) : ContentT(json) , duration(ContentT::originalInfoJson["duration"_ls].toInt()) -- cgit v1.2.3 From 2a6cbbff8246dd2f682624d1551facb2813394ad Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Jan 2019 21:08:40 +0900 Subject: RoomMessageEvent: easier creation of file-based events --- lib/events/roommessageevent.cpp | 53 +++++++++++++++++++++++++++++++++++++++++ lib/events/roommessageevent.h | 8 +++++++ 2 files changed, 61 insertions(+) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 69969c0f..d63ae2fe 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -21,6 +21,8 @@ #include "logging.h" #include +#include +#include using namespace QMatrixClient; using namespace EventContent; @@ -95,6 +97,38 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) { } +TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) +{ + auto filePath = file.absoluteFilePath(); + auto localUrl = QUrl::fromLocalFile(filePath); + auto mimeType = QMimeDatabase().mimeTypeForFile(file); + auto payloadSize = file.size(); + if (!asGenericFile) + { + auto mimeTypeName = mimeType.name(); + if (mimeTypeName.startsWith("image/")) + return new ImageContent(localUrl, payloadSize, mimeType, + QImageReader(filePath).size()); + + // duration can only be obtained asynchronously and can only be reliably + // done by starting to play the file. Left for a future implementation. + if (mimeTypeName.startsWith("video/")) + return new VideoContent(localUrl, payloadSize, mimeType); + + if (mimeTypeName.startsWith("audio/")) + return new AudioContent(localUrl, payloadSize, mimeType); + + } + return new FileContent(localUrl, payloadSize, mimeType); +} + +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, bool asGenericFile) + : RoomMessageEvent(plainBody, + asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file), + contentFromFile(file, asGenericFile)) +{ } + RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) { @@ -162,6 +196,25 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } +QString rawMsgTypeForMimeType(const QMimeType& mimeType) +{ + auto name = mimeType.name(); + return name.startsWith("image/") ? QStringLiteral("m.image") : + name.startsWith("video/") ? QStringLiteral("m.video") : + name.startsWith("audio/") ? QStringLiteral("m.audio") : + QStringLiteral("m.file"); +} + +QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url)); +} + +QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); +} + TextContent::TextContent(const QString& text, const QString& contentType) : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) { diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 5657135b..d5b570f5 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -21,6 +21,8 @@ #include "roomevent.h" #include "eventcontent.h" +class QFileInfo; + namespace QMatrixClient { namespace MessageEventContent = EventContent; // Back-compatibility @@ -49,6 +51,9 @@ namespace QMatrixClient explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, + bool asGenericFile = false); explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; @@ -68,6 +73,9 @@ namespace QMatrixClient bool hasFileContent() const; bool hasThumbnail() const; + static QString rawMsgTypeForUrl(const QUrl& url); + static QString rawMsgTypeForFile(const QFileInfo& fi); + private: QScopedPointer _content; -- cgit v1.2.3 From fb46c2d2a6e53557452837c2690f32a56387fcac Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Jan 2019 22:28:09 +0900 Subject: Room: findPendingEvent; fixes for postFile() --- lib/room.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++++---------------- lib/room.h | 5 +++- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 2c90955e..8f50607f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -599,6 +599,19 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const return timelineEdge(); } +Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) +{ + return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + +Room::PendingEvents::const_iterator +Room::findPendingEvent(const QString& txnId) const +{ + return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -1363,8 +1376,7 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, QString Room::retryMessage(const QString& txnId) { - auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; const auto& transferIt = d->fileTransfers.find(txnId); @@ -1418,7 +1430,7 @@ void Room::discardMessage(const QString& txnId) << "has been uploaded but the message was discarded"; } } - emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); + emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); } @@ -1445,28 +1457,29 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html, MessageEventType::Text); } -QString Room::postFile(const QString& plainText, const QUrl& localPath) +QString Room::postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile) { QFileInfo localFile { localPath.toLocalFile() }; Q_ASSERT(localFile.isFile()); - // Remote URL will only be known after upload, see below. - auto* content = new EventContent::FileContent(QUrl(), localFile.size(), - QMimeDatabase().mimeTypeForUrl(localPath)); - // TODO: Set the msgtype based on MIME type + // Remote URL will only be known after upload; fill in the local path + // to enable the preview while the event is pending. auto* pEvent = d->addAsPending( - makeEvent(plainText, "m.file", content)); + makeEvent(plainText, localFile, asGenericFile)); const auto txnId = pEvent->transactionId(); uploadFile(txnId, localPath); - QMetaObject::Connection c; - c = connect(this, &Room::fileTransferCompleted, this, - [c,this,pEvent,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + QMetaObject::Connection cCompleted, cCancelled; + cCompleted = connect(this, &Room::fileTransferCompleted, this, + [cCompleted,cCancelled,this,pEvent,txnId] + (const QString& id, QUrl, const QUrl& mxcUri) { if (id == txnId) { auto it = d->findAsPending(pEvent); if (it != d->unsyncedEvents.end()) { it->setFileUploaded(mxcUri); - emit pendingEventChanged(it - d->unsyncedEvents.begin()); + emit pendingEventChanged( + int(it - d->unsyncedEvents.begin())); d->doSendEvent(pEvent); } else { // Normally in this situation we should instruct @@ -1475,9 +1488,27 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath) qCWarning(MAIN) << "File uploaded to" << mxcUri << "but the event referring to it was cancelled"; } - disconnect(c); + disconnect(cCompleted); + disconnect(cCancelled); } }); + cCancelled = connect(this, &Room::fileTransferCancelled, this, + [cCompleted,cCancelled,this,pEvent,txnId] (const QString& id) { + if (id == txnId) + { + auto it = d->findAsPending(pEvent); + if (it != d->unsyncedEvents.end()) + { + emit pendingEventAboutToDiscard( + int(it - d->unsyncedEvents.begin())); + d->unsyncedEvents.erase(it); + emit pendingEventDiscarded(); + } + disconnect(cCompleted); + disconnect(cCancelled); + } + }); + return txnId; } @@ -1659,8 +1690,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) if (ongoingTransfer != d->fileTransfers.end() && ongoingTransfer->status == FileTransferInfo::Started) { - qCWarning(MAIN) << "Download for" << eventId - << "already started; to restart, cancel it first"; + qCWarning(MAIN) << "Transfer for" << eventId + << "is ongoing; download won't start"; return; } @@ -1923,11 +1954,15 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) break; it = nextPending + 1; - emit q->pendingEventAboutToMerge(nextPending->get(), - nextPendingPair.second - unsyncedEvents.begin()); + auto* nextPendingEvt = nextPending->get(); + emit q->pendingEventAboutToMerge(nextPendingEvt, + int(nextPendingPair.second - unsyncedEvents.begin())); qDebug(EVENTS) << "Merging pending event from transaction" - << (*nextPending)->transactionId() << "into" - << (*nextPending)->id(); + << nextPendingEvt->transactionId() << "into" + << nextPendingEvt->id(); + auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); + if (transfer.status != FileTransferInfo::None) + fileTransfers.insert(nextPendingEvt->id(), transfer); unsyncedEvents.erase(nextPendingPair.second); if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) { diff --git a/lib/room.h b/lib/room.h index 2b6a2172..029f87b7 100644 --- a/lib/room.h +++ b/lib/room.h @@ -234,6 +234,8 @@ namespace QMatrixClient rev_iter_t findInTimeline(TimelineItem::index_t index) const; rev_iter_t findInTimeline(const QString& evtId) const; + PendingEvents::iterator findPendingEvent(const QString & txnId); + PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; bool displayed() const; /// Mark the room as currently displayed to the user @@ -374,7 +376,8 @@ namespace QMatrixClient QString postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type); QString postHtmlText(const QString& plainText, const QString& html); - QString postFile(const QString& plainText, const QUrl& localPath); + QString postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile = false); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one -- cgit v1.2.3 From e3578b3cc6b978db1c04a1f684e1a03676c33f3b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 19:59:48 +0900 Subject: EventContent::ImageInfo: support originalFilename in POD constructor It's not mandated by the spec for anything except m.file but hey it's convenient. --- lib/events/eventcontent.cpp | 4 ++-- lib/events/eventcontent.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 03880885..9a5e872c 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -60,8 +60,8 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const } ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, - const QSize& imageSize) - : FileInfo(u, fileSize, mimeType), imageSize(imageSize) + const QSize& imageSize, const QString& originalFilename) + : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) { } ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 2e61276b..0588c0e2 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -129,7 +129,8 @@ namespace QMatrixClient public: explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, QMimeType mimeType = {}, - const QSize& imageSize = {}); + const QSize& imageSize = {}, + const QString& originalFilename = {}); ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); -- cgit v1.2.3 From 24c80a57fe1a79289f3028a81d6f8e0ac5f505fe Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 20:06:09 +0900 Subject: API version++; use QMediaResource from QtMultimedia (new dep) to detect m.video resolution The API version number should have been bumped long ago. --- .travis.yml | 1 + CMakeLists.txt | 8 +++++--- lib/events/roommessageevent.cpp | 17 ++++++++++------- libqmatrixclient.pri | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b2967cf..c0e8c097 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ addons: packages: - g++-5 - qt57base + - qt57multimedia - valgrind matrix: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a3193a4..c48a7ba9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,9 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.4.1 REQUIRED Network Gui) +# Qt 5.6+ is the formal requirement but for the sake of supporting UBPorts +# upstream Qt 5.4 is required. +find_package(Qt5 5.4.1 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) if (GTAD_PATH) @@ -140,7 +142,7 @@ add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS} ${libqmatrixclient_csdef_SRCS} ${libqmatrixclient_cswellknown_SRCS} ${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS}) -set(API_VERSION "0.4") +set(API_VERSION "0.5") set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.0") set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} ) set_property(TARGET QMatrixClient PROPERTY @@ -152,7 +154,7 @@ target_include_directories(QMatrixClient PUBLIC $ $ ) -target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui) +target_link_libraries(QMatrixClient Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) add_executable(qmc-example ${example_SRCS}) target_link_libraries(qmc-example Qt5::Core QMatrixClient) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index d63ae2fe..c3007fa0 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -23,6 +23,7 @@ #include #include #include +#include using namespace QMatrixClient; using namespace EventContent; @@ -102,24 +103,26 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) auto filePath = file.absoluteFilePath(); auto localUrl = QUrl::fromLocalFile(filePath); auto mimeType = QMimeDatabase().mimeTypeForFile(file); - auto payloadSize = file.size(); if (!asGenericFile) { auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) - return new ImageContent(localUrl, payloadSize, mimeType, - QImageReader(filePath).size()); + return new ImageContent(localUrl, file.size(), mimeType, + QImageReader(filePath).size(), + file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) - return new VideoContent(localUrl, payloadSize, mimeType); + return new VideoContent(localUrl, file.size(), mimeType, + QMediaResource(localUrl).resolution(), + file.fileName()); if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, payloadSize, mimeType); - + return new AudioContent(localUrl, file.size(), mimeType, + file.fileName()); } - return new FileContent(localUrl, payloadSize, mimeType); + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 8ca43e56..eefaec67 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -1,4 +1,4 @@ -QT += network +QT += network multimedia CONFIG += c++14 warn_on rtti_off create_prl object_parallel_to_source win32-msvc* { -- cgit v1.2.3 From e31bc6fe6b87562ea7879ab5ad1f8556f6df2d1d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 21:03:39 +0900 Subject: qmc-example: upgrade sendMesage() test; add sendFile() test Now really closes #267. --- examples/qmc-example.cpp | 151 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 10 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 652c1f92..7be82a28 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -20,7 +22,7 @@ using namespace std::placeholders; class QMCTest : public QObject { public: - QMCTest(Connection* conn, QString targetRoomName, QString source); + QMCTest(Connection* conn, QString testRoomName, QString source); private slots: void setupAndRun(); @@ -29,6 +31,9 @@ class QMCTest : public QObject void doTests(); void loadMembers(); void sendMessage(); + void sendFile(); + void checkFileSendingOutcome(const QString& txnId, + const QString& fileName); void addAndRemoveTag(); void sendAndRedact(); void checkRedactionOutcome(const QString& evtIdToRedact, @@ -47,6 +52,8 @@ class QMCTest : public QObject QString origin; QString targetRoomName; Room* targetRoom = nullptr; + + bool validatePendingEvent(const QString& txnId); }; #define QMC_CHECK(description, condition) \ @@ -69,6 +76,14 @@ class QMCTest : public QObject } \ } +bool QMCTest::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() && + it->deliveryStatus() == EventStatus::Submitted && + (*it)->transactionId() == txnId; +} + QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName)) { @@ -142,7 +157,8 @@ void QMCTest::run() { // TODO: Waiting for proper futures to come so that it could be: // targetRoom->postPlainText(origin % ": All tests finished") -// .then(this, &QMCTest::leave); +// .then(this, &QMCTest::leave); // Qt-style +// .then([this] { leave(); }); // STL-style auto txnId = targetRoom->postPlainText(origin % ": All tests finished"); connect(targetRoom, &Room::messageSent, this, @@ -166,6 +182,7 @@ void QMCTest::doTests() return; sendMessage(); + sendFile(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); @@ -206,19 +223,133 @@ void QMCTest::sendMessage() running.push_back("Message sending"); cout << "Sending a message" << endl; auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - auto& pending = targetRoom->pendingEvents(); - if (pending.empty()) + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; QMC_CHECK("Message sending", false); return; } - auto it = std::find_if(pending.begin(), pending.end(), - [&txnId] (const auto& e) { - return e->transactionId() == txnId; + + QMetaObject::Connection sc; + sc = connect(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,sc,txnId] (const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return; + + disconnect(sc); + + QMC_CHECK("Message sending", + is(*evt) && !evt->id().isEmpty() && + pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + }); +} + +void QMCTest::sendFile() +{ + running.push_back("File sending"); + cout << "Sending a file" << endl; + auto* tf = new QTemporaryFile; + if (!tf->open()) + { + cout << "Failed to create a temporary file" << endl; + QMC_CHECK("File sending", false); + return; + } + tf->write("Test"); + tf->close(); + // QFileInfo::fileName brings only the file name; QFile::fileName brings + // the full path + const auto tfName = QFileInfo(*tf).fileName(); + cout << "Sending file" << tfName.toStdString() << endl; + const auto txnId = targetRoom->postFile("Test file", + QUrl::fromLocalFile(tf->fileName())); + if (!validatePendingEvent(txnId)) + { + cout << "Invalid pending event right after submitting" << endl; + QMC_CHECK("File sending", false); + delete tf; + return; + } + + QMetaObject::Connection scCompleted, scFailed; + scCompleted = connect(targetRoom, &Room::fileTransferCompleted, this, + [this,txnId,tf,tfName,scCompleted,scFailed] (const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); + + if (id != txnId) + return; + + disconnect(scCompleted); + disconnect(scFailed); + delete tf; + + checkFileSendingOutcome(txnId, tfName); + }); + scFailed = connect(targetRoom, &Room::fileTransferFailed, this, + [this,txnId,tf,scCompleted,scFailed] + (const QString& id, const QString& error) { + if (id != txnId) + return; + + targetRoom->postPlainText(origin % ": File upload failed: " % error); + disconnect(scCompleted); + disconnect(scFailed); + delete tf; + + QMC_CHECK("File sending", false); + }); +} + +void QMCTest::checkFileSendingOutcome(const QString& txnId, + const QString& fileName) +{ + auto it = targetRoom->findPendingEvent(txnId); + if (it == targetRoom->pendingEvents().end()) + { + cout << "Pending file event dropped before upload completion" + << endl; + QMC_CHECK("File sending", false); + return; + } + if (it->deliveryStatus() != EventStatus::FileUploaded) + { + cout << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + QMC_CHECK("File sending", false); + return; + } + + QMetaObject::Connection sc; + sc = connect(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,sc,txnId,fileName] (const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return; + + cout << "Event " << txnId.toStdString() + << " arrived in the timeline" << endl; + disconnect(sc); + visit(*evt, + [&] (const RoomMessageEvent& e) { + QMC_CHECK("File sending", + !e.id().isEmpty() && + pendingEvents[size_t(pendingIdx)] + ->transactionId() == txnId && + e.hasFileContent() && + e.content()->fileInfo()->originalName == fileName); + }, + [this] (const RoomEvent&) { + QMC_CHECK("File sending", false); }); - QMC_CHECK("Message sending", it != pending.end()); - // TODO: Wait when it actually gets sent; check that it obtained an id - // Independently, check when it shows up in the timeline. + }); } void QMCTest::addAndRemoveTag() -- cgit v1.2.3 From 27555e44dfbaae26a0e030cb3c22eb00ba8371f0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 22:10:47 +0900 Subject: Add Qt5::Multimedia to examples/CMakeLists.txt too --- examples/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 49e0089a..cd5e15ed 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -45,7 +45,7 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.6 REQUIRED Network Gui) +find_package(Qt5 5.6 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) find_package(QMatrixClient REQUIRED) -- cgit v1.2.3 From e3a048ed3a8a5060affe6fcba1e1867294351177 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 5 Jan 2019 18:36:46 +0900 Subject: RoomEvent: don't log transactionId anymore It's already logged in Room - actually, several times at different stages. --- lib/events/roomevent.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 80d121de..3d03509f 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -42,10 +42,6 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& json) _redactedBecause = makeEvent(redaction.toObject()); return; } - - const auto& txnId = transactionId(); - if (!txnId.isEmpty()) - qCDebug(EVENTS) << "Event transactionId:" << txnId; } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job @@ -90,7 +86,6 @@ void RoomEvent::setTransactionId(const QString& txnId) auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); - qCDebug(EVENTS) << "New event transactionId:" << txnId; Q_ASSERT(transactionId() == txnId); } -- cgit v1.2.3 From a23fa6df08008f4a3f7437efa8afe839a102dc8e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 6 Jan 2019 17:43:37 +0900 Subject: visit(): pass decayed event types to is() So that is<> could be specialised for some types. --- lib/events/event.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/events/event.h b/lib/events/event.h index c51afcc4..d7ac4292 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -208,6 +208,9 @@ namespace QMatrixClient template inline auto registerEventType() { + // Initialise exactly once, even if this function is called twice for + // the same type (for whatever reason - you never know the ways of + // static initialisation is done). static const auto _ = setupFactory(); return _; // Only to facilitate usage in static initialisation } @@ -337,7 +340,8 @@ namespace QMatrixClient -> decltype(static_cast(&*eptr)) { Q_ASSERT(eptr); - return is(*eptr) ? static_cast(&*eptr) : nullptr; + return is>(*eptr) + ? static_cast(&*eptr) : nullptr; } // A single generic catch-all visitor @@ -369,7 +373,7 @@ namespace QMatrixClient visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t; - if (is(event)) + if (is>(event)) visitor(static_cast(event)); } @@ -383,7 +387,7 @@ namespace QMatrixClient fn_return_t&& defaultValue = {}) { using event_type = fn_arg_t; - if (is(event)) + if (is>(event)) return visitor(static_cast(event)); return std::forward>(defaultValue); } @@ -396,7 +400,7 @@ namespace QMatrixClient FnTs&&... visitors) { using event_type1 = fn_arg_t; - if (is(event)) + if (is>(event)) return visitor1(static_cast(event)); return visit(event, std::forward(visitor2), std::forward(visitors)...); -- cgit v1.2.3 From c83a73b17eaef465150458282e63a478ba238f26 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 6 Jan 2019 17:45:34 +0900 Subject: Create StateEventBase events if state_key is there This makes unknown state events to still be treated as state events. --- lib/events/stateevent.cpp | 13 ++++++++++++- lib/events/stateevent.h | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index fd8079be..c4151676 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -20,8 +20,19 @@ using namespace QMatrixClient; +// Aside from the normal factory to instantiate StateEventBase inheritors +// StateEventBase itself can be instantiated if there's a state_key JSON key +// but the event type is unknown. [[gnu::unused]] static auto stateEventTypeInitialised = - RoomEvent::factory_t::chainFactory(); + RoomEvent::factory_t::addMethod( + [] (const QJsonObject& json, const QString& matrixType) + { + if (auto e = StateEventBase::factory_t::make(json, matrixType)) + return e; + return json.contains("state_key") + ? makeEvent(unknownEventTypeId(), json) + : nullptr; + }); bool StateEventBase::repeatsState() const { diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index d488c0a0..dc017b11 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -38,6 +38,9 @@ namespace QMatrixClient { using StateEventPtr = event_ptr_tt; using StateEvents = EventsArray; + template <> + inline bool is(const Event& e) { return e.isStateEvent(); } + /** * A combination of event type and state key uniquely identifies a piece * of state in Matrix. -- cgit v1.2.3 From 0a09894add1c155279bfe5362d3a676f616b4530 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 6 Jan 2019 18:39:59 +0900 Subject: README.md: add/update badges --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72eaf7ab..e4303a27 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # libQMatrixClient +Made for Matrix + [![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) [![release](https://img.shields.io/github/release/QMatrixClient/libqmatrixclient/all.svg)](https://github.com/QMatrixClient/libqmatrixclient/releases/latest) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1023/badge)](https://bestpractices.coreinfrastructure.org/projects/1023) +[![](https://img.shields.io/cii/percentage/1023.svg)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) +![](https://img.shields.io/github/commit-activity/y/QMatrixClient/libQMatrixClient.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Tensor](https://matrix.org/docs/projects/client/tensor.html) and some other projects. +libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Spectral](https://matrix.org/docs/projects/client/spectral.html) and some other projects. ## Contacts You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org). -- cgit v1.2.3 From de292f1537863846c9bb43de65a3c1ef4f37d18d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 6 Jan 2019 19:41:42 +0900 Subject: README.md: make the CII badge a bit more prominent [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4303a27..58be072e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) [![release](https://img.shields.io/github/release/QMatrixClient/libqmatrixclient/all.svg)](https://github.com/QMatrixClient/libqmatrixclient/releases/latest) -[![](https://img.shields.io/cii/percentage/1023.svg)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) +[![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) ![](https://img.shields.io/github/commit-activity/y/QMatrixClient/libQMatrixClient.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -- cgit v1.2.3 From 5d570825262a0ac0f78deedbd393c5d258493dc2 Mon Sep 17 00:00:00 2001 From: qso Date: Sun, 6 Jan 2019 23:28:48 +0100 Subject: added info for QMATRIXCLIENT_INSTALL_EXAMPLE option to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 72eaf7ab..be63c994 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ You can install the library with CMake: cmake --build . --target install ``` This will also install cmake package config files; once this is done, you can use `examples/CMakeLists.txt` to compile the example with the _installed_ library. This file is a good starting point for your own CMake-based project using libQMatrixClient. +Installation of `qmc-example` application can be skipped by setting `QMATRIXCLIENT_INSTALL_EXAMPLE` to `OFF`. #### qmake-based The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: -- cgit v1.2.3 From 7b6ba76954f88558a638f174c68a87207fe4788d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 11:49:21 +0900 Subject: util.h: check for fallthrough attribute instead of C++ version --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index 9c9a37ba..336248d3 100644 --- a/lib/util.h +++ b/lib/util.h @@ -27,7 +27,7 @@ #include #include -#if __cplusplus >= 201703L +#if __has_cpp_attribute(fallthrough) #define FALLTHROUGH [[fallthrough]] #elif __has_cpp_attribute(clang::fallthrough) #define FALLTHROUGH [[clang::fallthrough]] -- cgit v1.2.3 From d5c07b98cd708d0bf4590e7fd249aa972b090461 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 13:32:04 +0900 Subject: Fix Omittables accidentally becoming non-omitted when compared with non-Omittable values --- lib/room.cpp | 5 +++-- lib/util.h | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 8f50607f..7ff8f5e9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -97,7 +97,7 @@ class Room::Private Connection* connection; QString id; JoinState joinState; - RoomSummary summary; + RoomSummary summary = { none, 0, none }; /// The state of the room at timeline position before-0 /// \sa timelineBase std::unordered_map baseState; @@ -1065,7 +1065,8 @@ int Room::joinedCount() const int Room::invitedCount() const { // TODO: Store invited users in Room too - return d->summary.invitedMemberCount; + Q_ASSERT(!d->summary.invitedMemberCount.omitted()); + return d->summary.invitedMemberCount.value(); } int Room::totalMemberCount() const diff --git a/lib/util.h b/lib/util.h index 336248d3..77c2bfdb 100644 --- a/lib/util.h +++ b/lib/util.h @@ -107,6 +107,25 @@ namespace QMatrixClient return *this; } + bool operator==(const value_type& rhs) const + { + return !omitted() && value() == rhs; + } + friend bool operator==(const value_type& lhs, + const Omittable& rhs) + { + return rhs == lhs; + } + bool operator!=(const value_type& rhs) const + { + return !operator==(rhs); + } + friend bool operator!=(const value_type& lhs, + const Omittable& rhs) + { + return !(rhs == lhs); + } + bool omitted() const { return _omitted; } const value_type& value() const { @@ -136,7 +155,7 @@ namespace QMatrixClient } value_type&& release() { _omitted = true; return std::move(_value); } - operator value_type&() & { return editValue(); } + operator const value_type&() const & { return value(); } const value_type* operator->() const & { return &value(); } value_type* operator->() & { return &editValue(); } const value_type& operator*() const & { return value(); } -- cgit v1.2.3 From 8dc2a3273ac3a5bb518fa987b25e3df15106c4d2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 13:34:31 +0900 Subject: Connection::provideRoom: allow omitting join state --- lib/connection.cpp | 21 +++++++++++++++++---- lib/connection.h | 31 +++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index c17cbffc..18fa91e7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -981,11 +981,12 @@ const ConnectionData* Connection::connectionData() const return d->data.get(); } -Room* Connection::provideRoom(const QString& id, JoinState joinState) +Room* Connection::provideRoom(const QString& id, Omittable joinState) { // TODO: This whole function is a strong case for a RoomManager class. Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); + // If joinState.omitted(), all joinState == comparisons below are false. const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); if (room) @@ -995,10 +996,19 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) // and emit a signal. For Invite and Join, there's no such problem. if (room->joinState() == joinState && joinState != JoinState::Leave) return room; + } else if (joinState.omitted()) + { + // No Join and Leave, maybe Invite? + room = d->roomMap.value({id, true}, nullptr); + if (room) + return room; + // No Invite either, setup a new room object below } - else + + if (!room) { - room = roomFactory()(this, id, joinState); + room = roomFactory()(this, id, + joinState.omitted() ? JoinState::Join : joinState.value()); if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; @@ -1010,6 +1020,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) this, &Connection::aboutToDeleteRoom); emit newRoom(room); } + if (joinState.omitted()) + return room; + if (joinState == JoinState::Invite) { // prev is either Leave or nullptr @@ -1018,7 +1031,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) } else { - room->setJoinState(joinState); + room->setJoinState(joinState.value()); // Preempt the Invite room (if any) with a room in Join/Leave state. auto* prevInvite = d->roomMap.take({id, true}); if (joinState == JoinState::Join) diff --git a/lib/connection.h b/lib/connection.h index ff3e2028..f2e10488 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -670,16 +670,27 @@ namespace QMatrixClient */ const ConnectionData* connectionData() const; - /** - * @brief Find a (possibly new) Room object for the specified id - * Use this method whenever you need to find a Room object in - * the local list of rooms. Note that this does not interact with - * the server; in particular, does not automatically create rooms - * on the server. - * @return a pointer to a Room object with the specified id; nullptr - * if roomId is empty or roomFactory() failed to create a Room object. - */ - Room* provideRoom(const QString& roomId, JoinState joinState); + /** Get a Room object for the given id in the given state + * + * Use this method when you need a Room object in the local list + * of rooms, with the given state. Note that this does not interact + * with the server; in particular, does not automatically create + * rooms on the server. This call performs necessary join state + * transitions; e.g., if it finds a room in Invite but + * `joinState == JoinState::Join` then the Invite room object + * will be deleted and a new room object with Join state created. + * In contrast, switching between Join and Leave happens within + * the same object. + * \param roomId room id (not alias!) + * \param joinState desired (target) join state of the room; if + * omitted, any state will be found and return unchanged, or a + * new Join room created. + * @return a pointer to a Room object with the specified id and the + * specified state; nullptr if roomId is empty or if roomFactory() + * failed to create a Room object. + */ + Room* provideRoom(const QString& roomId, + Omittable joinState = none); /** * Completes loading sync data. -- cgit v1.2.3 From 3f39c30bffcb4ebc8eefe9bd9feb1b71b1c13981 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 16:00:35 +0900 Subject: Room::Room: initialise display name So that the room has at least some display name before any events come to it. --- lib/room.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 7ff8f5e9..1931be49 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -89,11 +89,6 @@ class Room::Private Room* q; - // This updates the room displayname field (which is the way a room - // should be shown in the room list) It should be called whenever the - // list of members or the room name (m.room.name) or canonical alias change. - void updateDisplayname(); - Connection* connection; QString id; JoinState joinState; @@ -179,6 +174,14 @@ class Room::Private void renameMember(User* u, QString oldName); void removeMemberFromMap(const QString& username, User* u); + // This updates the room displayname field (which is the way a room + // should be shown in the room list); called whenever the list of + // members, the room name (m.room.name) or canonical alias change. + void updateDisplayname(); + // This is used by updateDisplayname() but only calculates the new name + // without any updates. + QString calculateDisplayname() const; + /// A point in the timeline corresponding to baseState rev_iter_t timelineBase() const { return q->findInTimeline(-1); } @@ -278,7 +281,6 @@ class Room::Private template users_shortlist_t buildShortlist(const ContT& users) const; users_shortlist_t buildShortlist(const QStringList& userIds) const; - QString calculateDisplayname() const; bool isLocalUser(const User* u) const { @@ -293,6 +295,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; + d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } -- cgit v1.2.3 From a9bdc89f66ba283859fd9ca7383a7256198174ed Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 13:36:57 +0900 Subject: Connection: fix/workaround glitches on joining/leaving Closes #273, in particular. --- lib/connection.cpp | 26 +++++++++++++++++++++++--- lib/connection.h | 7 ++++--- lib/room.cpp | 3 ++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 18fa91e7..c582cf94 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -84,6 +84,7 @@ class Connection::Private QHash, Room*> roomMap; QVector roomIdsToForget; QVector firstTimeRooms; + QVector pendingStateRoomIds; QMap userMap; DirectChatsMap directChats; DirectChatUsersMap directChatUsers; @@ -339,6 +340,7 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { } if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) { + d->pendingStateRoomIds.removeOne(roomData.roomId); r->updateData(std::move(roomData), fromCache); if (d->firstTimeRooms.removeOne(r)) emit loadedRoomState(r); @@ -427,14 +429,32 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { auto job = callApi(roomAlias, serverNames); + // Upon completion, ensure a room object in Join state is created but only + // if it's not already there due to a sync completing earlier. connect(job, &JoinRoomJob::success, - this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); + this, [this, job] { provideRoom(job->roomId()); }); return job; } -void Connection::leaveRoom(Room* room) +LeaveRoomJob* Connection::leaveRoom(Room* room) { - callApi(room->id()); + const auto& roomId = room->id(); + const auto job = callApi(roomId); + if (room->joinState() == JoinState::Invite) + { + // Workaround matrix-org/synapse#2181 - if the room is in invite state + // the invite may have been cancelled but Synapse didn't send it in + // `/sync`. See also #273 for the discussion in the library context. + d->pendingStateRoomIds.push_back(roomId); + connect(job, &LeaveRoomJob::success, this, [this,roomId] { + if (d->pendingStateRoomIds.removeOne(roomId)) + { + qCDebug(MAIN) << "Forcing the room to Leave status"; + provideRoom(roomId, JoinState::Leave); + } + }); + } + return job; } inline auto splitMediaId(const QString& mediaId) diff --git a/lib/connection.h b/lib/connection.h index f2e10488..cba57e3d 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -48,6 +48,7 @@ namespace QMatrixClient class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; + class LeaveRoomJob; /** Create a single-shot connection that triggers on the signal and * then self-disconnects @@ -494,14 +495,14 @@ namespace QMatrixClient SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event) const; + /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ + virtual LeaveRoomJob* leaveRoom( Room* room ); + // Old API that will be abolished any time soon. DO NOT USE. /** @deprecated Use callApi() or Room::postReceipt() instead */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; - /** @deprecated Use callApi() or Room::leaveRoom() instead */ - virtual void leaveRoom( Room* room ); - signals: /** * @deprecated diff --git a/lib/room.cpp b/lib/room.cpp index 1931be49..d806183f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1633,7 +1633,8 @@ void Room::inviteToRoom(const QString& memberId) LeaveRoomJob* Room::leaveRoom() { - return connection()->callApi(id()); + // FIXME, #63: It should be RoomManager, not Connection + return connection()->leaveRoom(this); } SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const -- cgit v1.2.3 From 007bd03300666ccab6d7887f5987df2a0085bab1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 11:49:21 +0900 Subject: util.h: check for fallthrough attribute instead of C++ version --- lib/util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index 9c9a37ba..336248d3 100644 --- a/lib/util.h +++ b/lib/util.h @@ -27,7 +27,7 @@ #include #include -#if __cplusplus >= 201703L +#if __has_cpp_attribute(fallthrough) #define FALLTHROUGH [[fallthrough]] #elif __has_cpp_attribute(clang::fallthrough) #define FALLTHROUGH [[clang::fallthrough]] -- cgit v1.2.3 From 4824705ea4eddfdb5d3845a64a96a1f5e2c022d0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 13:40:12 +0900 Subject: qmc-example: improve conclusion code Make the HTML version of the report and send it to the room if available (tests HTML outlooks along the way). --- examples/qmc-example.cpp | 73 +++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 8fbf4824..372a80ad 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -41,7 +41,7 @@ class QMCTest : public QObject void markDirectChat(); void checkDirectChatOutcome( const Connection::DirectChatsMap& added); - void leave(); + void conclude(); void finalize(); private: @@ -95,7 +95,7 @@ QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) connect(c.data(), &Connection::connected, this, &QMCTest::setupAndRun); connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); // Big countdown watchdog - QTimer::singleShot(180000, this, &QMCTest::leave); + QTimer::singleShot(180000, this, &QMCTest::conclude); } void QMCTest::setupAndRun() @@ -110,7 +110,7 @@ void QMCTest::setupAndRun() running.push_back("Join room"); auto joinJob = c->joinRoom(targetRoomName); connect(joinJob, &BaseJob::failure, this, - [this] { QMC_CHECK("Join room", false); finalize(); }); + [this] { QMC_CHECK("Join room", false); conclude(); }); // Connection::joinRoom() creates a Room object upon JoinRoomJob::success // but this object is empty until the first sync is done. connect(joinJob, &BaseJob::success, this, [this,joinJob] { @@ -149,26 +149,12 @@ void QMCTest::run() c->sync(); connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests); connect(c.data(), &Connection::syncDone, c.data(), [this] { - cout << "Sync complete, " - << running.size() << " tests in the air" << endl; + cout << "Sync complete, " << running.size() << " test(s) in the air: " + << running.join(", ").toStdString() << endl; if (!running.isEmpty()) c->sync(10000); - else if (targetRoom) - { - // TODO: Waiting for proper futures to come so that it could be: -// targetRoom->postPlainText(origin % ": All tests finished") -// .then(this, &QMCTest::leave); // Qt-style -// .then([this] { leave(); }); // STL-style - auto txnId = - targetRoom->postPlainText(origin % ": All tests finished"); - connect(targetRoom, &Room::messageSent, this, - [this,txnId] (QString serverTxnId) { - if (txnId == serverTxnId) - leave(); - }); - } else - finalize(); + conclude(); }); } @@ -469,13 +455,47 @@ void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id())); } -void QMCTest::leave() +void QMCTest::conclude() { + auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; + if (!failed.isEmpty() || !running.isEmpty()) + succeededRec += " of " % + QString::number(succeeded.size() + failed.size() + running.size()) % + " total"; + QString plainReport = origin % ": Testing complete, " % succeededRec; + QString color = failed.isEmpty() && running.isEmpty() ? "00AA00" : "AA0000"; + QString htmlReport = origin % ": Testing complete, " % + succeededRec; + if (!failed.isEmpty()) + { + plainReport += "\nFAILED: " % failed.join(", "); + htmlReport += "
Failed: " % failed.join(", "); + } + if (!running.isEmpty()) + { + plainReport += "\nDID NOT FINISH: " % running.join(", "); + htmlReport += + "
Did not finish: " % running.join(", "); + } + cout << plainReport.toStdString() << endl; + if (targetRoom) { - cout << "Leaving the room" << endl; - connect(targetRoom->leaveRoom(), &BaseJob::finished, - this, &QMCTest::finalize); + // TODO: Waiting for proper futures to come so that it could be: +// targetRoom->postHtmlText(...) +// .then(this, &QMCTest::finalize); // Qt-style or +// .then([this] { finalize(); }); // STL-style + auto txnId = targetRoom->postHtmlText(plainReport, htmlReport); + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (QString serverTxnId) { + if (txnId != serverTxnId) + return; + + cout << "Leaving the room" << endl; + connect(targetRoom->leaveRoom(), &BaseJob::finished, + this, &QMCTest::finalize); + }); } else finalize(); @@ -487,11 +507,6 @@ void QMCTest::finalize() c->logout(); connect(c.data(), &Connection::loggedOut, qApp, [this] { - if (!failed.isEmpty()) - cout << "FAILED: " << failed.join(", ").toStdString() << endl; - if (!running.isEmpty()) - cout << "DID NOT FINISH: " - << running.join(", ").toStdString() << endl; QCoreApplication::processEvents(); QCoreApplication::exit(failed.size() + running.size()); }); -- cgit v1.2.3 From a5267dbaa22581e316f440dc7327f2e7431012d5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 11:48:06 +0900 Subject: qt_connection_util.h: a new home for connectSingleShot() and newly made connectUntil() --- lib/connection.h | 21 +--------- lib/qt_connection_util.h | 102 +++++++++++++++++++++++++++++++++++++++++++++++ lib/util.h | 33 ++------------- libqmatrixclient.pri | 1 + 4 files changed, 108 insertions(+), 49 deletions(-) create mode 100644 lib/qt_connection_util.h diff --git a/lib/connection.h b/lib/connection.h index ff3e2028..98e8dced 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -21,6 +21,7 @@ #include "csapi/create_room.h" #include "joinstate.h" #include "events/accountdataevents.h" +#include "qt_connection_util.h" #include #include @@ -49,26 +50,6 @@ namespace QMatrixClient class SendToDeviceJob; class SendMessageJob; - /** Create a single-shot connection that triggers on the signal and - * then self-disconnects - * - * Only supports DirectConnection type. - */ - template - inline auto connectSingleShot(SenderT1* sender, SignalT signal, - ReceiverT2* receiver, SlotT slot) - { - QMetaObject::Connection connection; - connection = QObject::connect(sender, signal, receiver, slot, - Qt::DirectConnection); - Q_ASSERT(connection); - QObject::connect(sender, signal, receiver, - [connection] { QObject::disconnect(connection); }, - Qt::DirectConnection); - return connection; - } - class Connection; using room_factory_t = std::function + * + * 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 "util.h" + +#include + +namespace QMatrixClient { + namespace _impl { + template + inline QMetaObject::Connection connectUntil( + SenderT* sender, SignalT signal, ContextT* context, + std::function slot, Qt::ConnectionType connType) + { + auto pc = std::make_unique(); + auto& c = *pc; // Resolve a reference before pc is moved to lambda + c = QObject::connect(sender, signal, context, + [pc=std::move(pc),slot] (ArgTs... args) { + Q_ASSERT(*pc); + if (slot(std::forward(args)...)) + QObject::disconnect(*pc); + }, connType); + return c; + } + } + + template + inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, + const FunctorT& slot, + Qt::ConnectionType connType = Qt::AutoConnection) + { + return _impl::connectUntil(sender, signal, context, + typename function_traits::function_type(slot), + connType); + } + + /** Create a single-shot connection that triggers on the signal and + * then self-disconnects + * + * Only supports DirectConnection type. + */ + template + inline auto connectSingleShot(SenderT* sender, SignalT signal, + ReceiverT* receiver, SlotT slot) + { + QMetaObject::Connection connection; + connection = QObject::connect(sender, signal, receiver, slot, + Qt::DirectConnection); + Q_ASSERT(connection); + QObject::connect(sender, signal, receiver, + [connection] { QObject::disconnect(connection); }, + Qt::DirectConnection); + return connection; + } + + /** A guard pointer that disconnects an interested object upon destruction + * It's almost QPointer<> except that you have to initialise it with one + * more additional parameter - a pointer to a QObject that will be + * disconnected from signals of the underlying pointer upon the guard's + * destruction. + */ + template + class ConnectionsGuard : public QPointer + { + public: + ConnectionsGuard(T* publisher, QObject* subscriber) + : QPointer(publisher), subscriber(subscriber) + { } + ~ConnectionsGuard() + { + if (*this) + (*this)->disconnect(subscriber); + } + ConnectionsGuard(ConnectionsGuard&&) = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) = default; + Q_DISABLE_COPY(ConnectionsGuard) + using QPointer::operator=; + + private: + QObject* subscriber; + }; +} diff --git a/lib/util.h b/lib/util.h index 336248d3..bae7f93f 100644 --- a/lib/util.h +++ b/lib/util.h @@ -18,8 +18,9 @@ #pragma once -#include -#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0)) +#include + +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) #include #include #endif @@ -166,6 +167,7 @@ namespace QMatrixClient static constexpr auto is_callable = true; using return_type = ReturnT; using arg_types = std::tuple; + using function_type = std::function; static constexpr auto arg_number = std::tuple_size::value; }; @@ -265,33 +267,6 @@ namespace QMatrixClient return std::make_pair(last, sLast); } - /** A guard pointer that disconnects an interested object upon destruction - * It's almost QPointer<> except that you have to initialise it with one - * more additional parameter - a pointer to a QObject that will be - * disconnected from signals of the underlying pointer upon the guard's - * destruction. - */ - template - class ConnectionsGuard : public QPointer - { - public: - ConnectionsGuard(T* publisher, QObject* subscriber) - : QPointer(publisher), subscriber(subscriber) - { } - ~ConnectionsGuard() - { - if (*this) - (*this)->disconnect(subscriber); - } - ConnectionsGuard(ConnectionsGuard&&) = default; - ConnectionsGuard& operator=(ConnectionsGuard&&) = default; - Q_DISABLE_COPY(ConnectionsGuard) - using QPointer::operator=; - - private: - QObject* subscriber; - }; - /** Pretty-prints plain text into HTML * This includes HTML escaping of <,>,",& and URLs linkification. */ diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index eefaec67..f523f3a2 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -19,6 +19,7 @@ HEADERS += \ $$SRCPATH/avatar.h \ $$SRCPATH/syncdata.h \ $$SRCPATH/util.h \ + $$SRCPATH/qt_connection_util.h \ $$SRCPATH/events/event.h \ $$SRCPATH/events/roomevent.h \ $$SRCPATH/events/stateevent.h \ -- cgit v1.2.3 From 5544331af35bb3f0533975611d1e432ba6817a5c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 10 Jan 2019 16:51:44 +0900 Subject: qmc-example: use connectUntil() --- examples/qmc-example.cpp | 77 +++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 372a80ad..4b39b032 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -36,8 +36,7 @@ class QMCTest : public QObject const QString& fileName); void addAndRemoveTag(); void sendAndRedact(); - void checkRedactionOutcome(const QString& evtIdToRedact, - const QMetaObject::Connection& sc); + bool checkRedactionOutcome(const QString& evtIdToRedact); void markDirectChat(); void checkDirectChatOutcome( const Connection::DirectChatsMap& added); @@ -216,21 +215,19 @@ void QMCTest::sendMessage() return; } - QMetaObject::Connection sc; - sc = connect(targetRoom, &Room::pendingEventAboutToMerge, this, - [this,sc,txnId] (const RoomEvent* evt, int pendingIdx) { + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,txnId] (const RoomEvent* evt, int pendingIdx) { const auto& pendingEvents = targetRoom->pendingEvents(); Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); if (evt->transactionId() != txnId) - return; - - disconnect(sc); + return false; QMC_CHECK("Message sending", is(*evt) && !evt->id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() == evt->transactionId()); + return true; }); } @@ -261,30 +258,26 @@ void QMCTest::sendFile() return; } - QMetaObject::Connection scCompleted, scFailed; - scCompleted = connect(targetRoom, &Room::fileTransferCompleted, this, - [this,txnId,tf,tfName,scCompleted,scFailed] (const QString& id) { + // FIXME: Clean away connections (connectUntil doesn't help here). + connect(targetRoom, &Room::fileTransferCompleted, this, + [this,txnId,tf,tfName] (const QString& id) { auto fti = targetRoom->fileTransferInfo(id); Q_ASSERT(fti.status == FileTransferInfo::Completed); if (id != txnId) return; - disconnect(scCompleted); - disconnect(scFailed); delete tf; checkFileSendingOutcome(txnId, tfName); }); - scFailed = connect(targetRoom, &Room::fileTransferFailed, this, - [this,txnId,tf,scCompleted,scFailed] + connect(targetRoom, &Room::fileTransferFailed, this, + [this,txnId,tf] (const QString& id, const QString& error) { if (id != txnId) return; targetRoom->postPlainText(origin % ": File upload failed: " % error); - disconnect(scCompleted); - disconnect(scFailed); delete tf; QMC_CHECK("File sending", false); @@ -311,18 +304,16 @@ void QMCTest::checkFileSendingOutcome(const QString& txnId, return; } - QMetaObject::Connection sc; - sc = connect(targetRoom, &Room::pendingEventAboutToMerge, this, - [this,sc,txnId,fileName] (const RoomEvent* evt, int pendingIdx) { + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,txnId,fileName] (const RoomEvent* evt, int pendingIdx) { const auto& pendingEvents = targetRoom->pendingEvents(); Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); if (evt->transactionId() != txnId) - return; + return false; - cout << "Event " << txnId.toStdString() + cout << "File event " << txnId.toStdString() << " arrived in the timeline" << endl; - disconnect(sc); visit(*evt, [&] (const RoomMessageEvent& e) { QMC_CHECK("File sending", @@ -335,6 +326,7 @@ void QMCTest::checkFileSendingOutcome(const QString& txnId, [this] (const RoomEvent&) { QMC_CHECK("File sending", false); }); + return true; }); } @@ -356,7 +348,7 @@ void QMCTest::addAndRemoveTag() cout << "Test tag set, removing it now" << endl; targetRoom->removeTag(TestTag); QMC_CHECK("Tagging test", !targetRoom->tags().contains(TestTag)); - QObject::disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); + disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); } }); cout << "Adding a tag" << endl; @@ -380,41 +372,40 @@ void QMCTest::sendAndRedact() cout << "Redacting the message" << endl; targetRoom->redactEvent(evtId, origin); - QMetaObject::Connection sc; - sc = connect(targetRoom, &Room::addedMessages, this, - [this,sc,evtId] { checkRedactionOutcome(evtId, sc); }); + + connectUntil(targetRoom, &Room::addedMessages, this, + [this,evtId] { return checkRedactionOutcome(evtId); }); }); } -void QMCTest::checkRedactionOutcome(const QString& evtIdToRedact, - const QMetaObject::Connection& sc) +bool QMCTest::checkRedactionOutcome(const QString& evtIdToRedact) { // There are two possible (correct) outcomes: either the event comes already // redacted at the next sync, or the nearest sync completes with // the unredacted event but the next one brings redaction. auto it = targetRoom->findInTimeline(evtIdToRedact); if (it == targetRoom->timelineEdge()) - return; // Waiting for the next sync + return false; // Waiting for the next sync if ((*it)->isRedacted()) { cout << "The sync brought already redacted message" << endl; QMC_CHECK("Redaction", true); - disconnect(sc); - return; - } - cout << "Message came non-redacted with the sync, waiting for redaction" - << endl; - connect(targetRoom, &Room::replacedEvent, this, - [this,evtIdToRedact] - (const RoomEvent* newEvent, const RoomEvent* oldEvent) { - if (oldEvent->id() == evtIdToRedact) - { + } else { + cout << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connectUntil(targetRoom, &Room::replacedEvent, this, + [this,evtIdToRedact] + (const RoomEvent* newEvent, const RoomEvent* oldEvent) { + if (oldEvent->id() != evtIdToRedact) + return false; + QMC_CHECK("Redaction", newEvent->isRedacted() && newEvent->redactionReason() == origin); - disconnect(targetRoom, &Room::replacedEvent, nullptr, nullptr); - } - }); + return true; + }); + } + return true; } void QMCTest::markDirectChat() -- cgit v1.2.3 From 3cb7982fda8c0049eff51a9ab65eb43667e2c4ce Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 13 Jan 2019 17:53:22 +0900 Subject: Fix building with Qt before 5.10 See https://bugreports.qt.io/browse/QTBUG-60339 --- lib/qt_connection_util.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 2df2b186..c2bde8df 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -30,11 +30,16 @@ namespace QMatrixClient { SenderT* sender, SignalT signal, ContextT* context, std::function slot, Qt::ConnectionType connType) { + // See https://bugreports.qt.io/browse/QTBUG-60339 +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + auto pc = std::make_shared(); +#else auto pc = std::make_unique(); +#endif auto& c = *pc; // Resolve a reference before pc is moved to lambda c = QObject::connect(sender, signal, context, [pc=std::move(pc),slot] (ArgTs... args) { - Q_ASSERT(*pc); + Q_ASSERT(*pc); // If it's been triggered, it should exist if (slot(std::forward(args)...)) QObject::disconnect(*pc); }, connType); -- cgit v1.2.3 From 4c30996f28bfb6507eb5fb6f730a8769f8d964e3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 10 Jan 2019 16:46:57 +0900 Subject: Security fix: require that state events have state_key This has been fixed in the past but got undone after the great remaking of the event types system. Further commits will introduce tests to make sure this does not get undone again. --- lib/events/stateevent.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index c4151676..e96614d2 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -25,13 +25,15 @@ using namespace QMatrixClient; // but the event type is unknown. [[gnu::unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( - [] (const QJsonObject& json, const QString& matrixType) + [] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr { + if (!json.contains("state_key")) + return nullptr; + if (auto e = StateEventBase::factory_t::make(json, matrixType)) return e; - return json.contains("state_key") - ? makeEvent(unknownEventTypeId(), json) - : nullptr; + + return makeEvent(unknownEventTypeId(), json); }); bool StateEventBase::repeatsState() const -- cgit v1.2.3 From e2c0148960e6e2b4595599de94d7a867f13782a0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 10 Jan 2019 16:52:31 +0900 Subject: qmc-example: add setTopic test for true and fake state changes --- examples/qmc-example.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 4b39b032..421ead27 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -5,6 +5,7 @@ #include "csapi/room_send.h" #include "csapi/joining.h" #include "csapi/leaving.h" +#include "events/simplestateevents.h" #include #include @@ -34,6 +35,7 @@ class QMCTest : public QObject void sendFile(); void checkFileSendingOutcome(const QString& txnId, const QString& fileName); + void setTopic(); void addAndRemoveTag(); void sendAndRedact(); bool checkRedactionOutcome(const QString& evtIdToRedact); @@ -168,6 +170,7 @@ void QMCTest::doTests() sendMessage(); sendFile(); + setTopic(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); @@ -327,6 +330,46 @@ void QMCTest::checkFileSendingOutcome(const QString& txnId, QMC_CHECK("File sending", false); }); return true; + }); +} + +void QMCTest::setTopic() +{ + static const char* const stateTestName = "State setting test"; + static const char* const fakeStateTestName = "Fake state event immunity test"; + running.push_back(stateTestName); + running.push_back(fakeStateTestName); + auto initialTopic = targetRoom->topic(); + + const auto newTopic = c->generateTxnId(); + targetRoom->setTopic(newTopic); // Sets the state by proper means + const auto fakeTopic = c->generateTxnId(); + targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event + RoomTopicEvent(fakeTopic).contentJson()); + + connectUntil(targetRoom, &Room::topicChanged, this, + [this,newTopic,fakeTopic,initialTopic] { + if (targetRoom->topic() == newTopic) + { + QMC_CHECK(stateTestName, true); + // Don't reset the topic yet if the negative test still runs + if (!running.contains(fakeStateTestName)) + targetRoom->setTopic(initialTopic); + + return true; + } + return false; + }); + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,fakeTopic,initialTopic] (const RoomEvent* e, int) { + if (e->contentJson().value("topic").toString() != fakeTopic) + return false; // Wait on for the right event + + QMC_CHECK(fakeStateTestName, !e->isStateEvent()); + if (!running.contains(fakeStateTestName)) + targetRoom->setTopic(initialTopic); + return true; }); } -- cgit v1.2.3 From f438d37b169965ee0a9937b5178560a653f1197b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 15 Jan 2019 16:43:24 +0900 Subject: .travis.yml: Use ninja on Linux --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc143a62..6926f4ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ addons: - ubuntu-toolchain-r-test - sourceline: 'ppa:beineri/opt-qt571-trusty' packages: + - ninja-build - g++-5 - qt57base - qt57multimedia @@ -27,19 +28,19 @@ matrix: before_install: - eval "${ENV_EVAL}" -- if [ "$TRAVIS_OS_NAME" = "linux" ]; then VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt57/bin/qt57-env.sh; fi +- if [ "$TRAVIS_OS_NAME" = "linux" ]; then USE_NINJA="-GNinja"; VALGRIND="valgrind $VALGRIND_OPTIONS"; . /opt/qt57/bin/qt57-env.sh; fi install: - git clone https://github.com/QMatrixClient/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - pushd gtad -- cmake -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} . +- cmake $USE_NINJA -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} . - cmake --build . - popd before_script: - mkdir build && pushd build -- cmake -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_PREFIX=../install .. +- cmake $USE_NINJA -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_PREFIX=../install .. - cmake --build . --target update-api - popd -- cgit v1.2.3 From cc7d034fa67196ad4950d3785aff64e4c5765855 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Wed, 30 Jan 2019 19:30:07 +0300 Subject: Connection: infinite sync loop logic by default --- lib/connection.cpp | 13 ++++++++++++- lib/connection.h | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index c582cf94..982145f7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -133,6 +133,8 @@ Connection::Connection(const QUrl& server, QObject* parent) , d(std::make_unique(std::make_unique(server))) { d->q = this; // All d initialization should occur before this line + // sync loop: + connect(this, &Connection::syncDone, this, &Connection::getNewEvents); } Connection::Connection(QObject* parent) @@ -250,7 +252,7 @@ void Connection::Private::connectWithToken(const QString& user, << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); - + q->sync(); // initial sync after connection } void Connection::checkAndConnect(const QString& userId, @@ -406,6 +408,15 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { } } +void Connection::getNewEvents() +{ + // Borrowed the logic from Quiark's code in Tensor + // to cache not too aggressively and not on the first sync. + if (++_saveStateCounter % 17 == 2) + saveState(); + sync(30*1000); +} + void Connection::stopSync() { if (d->syncJob) diff --git a/lib/connection.h b/lib/connection.h index 9e4121f4..dcc77824 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -678,6 +678,7 @@ namespace QMatrixClient * Completes loading sync data. */ void onSyncSuccess(SyncData &&data, bool fromCache = false); + void getNewEvents(); private: class Private; @@ -702,6 +703,8 @@ namespace QMatrixClient static room_factory_t _roomFactory; static user_factory_t _userFactory; + + int _saveStateCounter = 0; }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::Connection*) -- cgit v1.2.3 From 22dd5f1e8988b03a691487cdad164a82a36e7f8c Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Sat, 2 Feb 2019 23:51:20 +0300 Subject: Connection: separated sync loop logic with delay control Signed-off-by: Alexey Andreyev --- lib/connection.cpp | 69 +++++++++++++++++++++++++++++++++++++++++++++++------- lib/connection.h | 30 +++++++++++++++++++++++- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 982145f7..e7f9e4b2 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -45,6 +45,7 @@ #include #include #include +#include using namespace QMatrixClient; @@ -133,8 +134,6 @@ Connection::Connection(const QUrl& server, QObject* parent) , d(std::make_unique(std::make_unique(server))) { d->q = this; // All d initialization should occur before this line - // sync loop: - connect(this, &Connection::syncDone, this, &Connection::getNewEvents); } Connection::Connection(QObject* parent) @@ -232,6 +231,11 @@ void Connection::doConnectToServer(const QString& user, const QString& password, }); } +void Connection::syncLoopIteration() +{ + sync(_syncLoopTimeout); +} + void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) @@ -252,7 +256,6 @@ void Connection::Private::connectWithToken(const QString& user, << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); - q->sync(); // initial sync after connection } void Connection::checkAndConnect(const QString& userId, @@ -321,6 +324,15 @@ void Connection::sync(int timeout) }); } +void Connection::syncLoop(int timeout) +{ + _syncLoopTimeout = timeout; + connect(this, &Connection::syncDone, this, &Connection::getNewEventsOnSyncDone); + connect(this, &Connection::syncError, this, &Connection::getNewEventsOnSyncError); + _syncLoopElapsedTimer.start(); + sync(_syncLoopTimeout); // initial sync to start the loop +} + void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) @@ -410,11 +422,33 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { void Connection::getNewEvents() { - // Borrowed the logic from Quiark's code in Tensor - // to cache not too aggressively and not on the first sync. - if (++_saveStateCounter % 17 == 2) - saveState(); - sync(30*1000); + int delay = minSyncLoopDelayMs() - _syncLoopElapsedTimer.restart(); + if (delay<0) { + delay = 0; + } + QTimer::singleShot(delay, this, &Connection::syncLoopIteration); +} + +void Connection::getNewEventsOnSyncDone() +{ + if (_prevSyncLoopIterationDone) { + _syncLoopAttemptNumber++; + } else { + _syncLoopAttemptNumber = 0; + } + emit syncAttemptNumberChanged(_syncLoopAttemptNumber); + getNewEvents(); +} + +void Connection::getNewEventsOnSyncError() +{ + if (_prevSyncLoopIterationDone) { + _syncLoopAttemptNumber = 0; + } else { + _syncLoopAttemptNumber++; + } + emit syncAttemptNumberChanged(_syncLoopAttemptNumber); + getNewEvents(); } void Connection::stopSync() @@ -436,6 +470,15 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const return callApi(room->id(), "m.read", event->id()); } +void Connection::setMinSyncDelayMs(qint64 minSyncDelayMs) +{ + if (_minSyncLoopDelayMs == minSyncDelayMs) + return; + + _minSyncLoopDelayMs = minSyncDelayMs; + emit minSyncDelayMsChanged(_minSyncLoopDelayMs); +} + JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { @@ -1100,6 +1143,16 @@ user_factory_t Connection::userFactory() return _userFactory; } +qint64 Connection::minSyncLoopDelayMs() const +{ + return _minSyncLoopDelayMs; +} + +uint Connection::syncLoopAttemptNumber() const +{ + return _syncLoopAttemptNumber; +} + room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); user_factory_t Connection::_userFactory = defaultUserFactory<>(); diff --git a/lib/connection.h b/lib/connection.h index dcc77824..ee7ad243 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -105,6 +106,8 @@ namespace QMatrixClient Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + Q_PROPERTY(qint64 minSyncLoopDelayMs READ minSyncLoopDelayMs WRITE setMinSyncDelayMs NOTIFY minSyncDelayMsChanged) + Q_PROPERTY(uint syncLoopAttemptNumber READ syncLoopAttemptNumber NOTIFY syncAttemptNumberChanged) public: // Room ids, rather than room pointers, are used in the direct chat @@ -353,6 +356,11 @@ namespace QMatrixClient template static void setUserType() { setUserFactory(defaultUserFactory()); } + qint64 minSyncLoopDelayMs() const; + void setMinSyncDelayMs(qint64 minSyncLoopDelayMs); + + uint syncLoopAttemptNumber() const; + public slots: /** Set the homeserver base URL */ void setHomeserver(const QUrl& baseUrl); @@ -371,6 +379,15 @@ namespace QMatrixClient void logout(); void sync(int timeout = -1); + + /** Start sync loop with the minSyncLoopDelayMs value + where minSyncLoopDelayMs could be changed on the client + according to syncDone/syncError signals and + the syncLoopAttemptNumber counter. + The syncLoopAttemptNumber counter is resetting + after non-repeating syncDone/syncError events*/ + void syncLoop(int timeout = -1); + void stopSync(); QString nextBatchToken() const; @@ -645,6 +662,8 @@ namespace QMatrixClient void cacheStateChanged(); void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); + void minSyncDelayMsChanged(qint64 minSyncLoopDelayMs); + void syncAttemptNumberChanged(uint syncLoopAttemptNumber); protected: /** @@ -678,7 +697,11 @@ namespace QMatrixClient * Completes loading sync data. */ void onSyncSuccess(SyncData &&data, bool fromCache = false); + + protected slots: void getNewEvents(); + void getNewEventsOnSyncDone(); + void getNewEventsOnSyncError(); private: class Private; @@ -704,7 +727,12 @@ namespace QMatrixClient static room_factory_t _roomFactory; static user_factory_t _userFactory; - int _saveStateCounter = 0; + QElapsedTimer _syncLoopElapsedTimer; + int _syncLoopTimeout = -1; + qint64 _minSyncLoopDelayMs = 0; + void syncLoopIteration(); + uint _syncLoopAttemptNumber = 0; + bool _prevSyncLoopIterationDone = false; }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::Connection*) -- cgit v1.2.3 From bf6cd3d29052f9e79fee29c6a6646d95271a196a Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Mon, 4 Feb 2019 11:58:43 +0300 Subject: Connection: simplified sync loop logic without delays Signed-off-by: Alexey Andreyev --- lib/connection.cpp | 58 +++--------------------------------------------------- lib/connection.h | 26 +----------------------- 2 files changed, 4 insertions(+), 80 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index e7f9e4b2..a9a8bba3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -45,7 +45,6 @@ #include #include #include -#include using namespace QMatrixClient; @@ -256,6 +255,7 @@ void Connection::Private::connectWithToken(const QString& user, << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); + } void Connection::checkAndConnect(const QString& userId, @@ -327,10 +327,8 @@ void Connection::sync(int timeout) void Connection::syncLoop(int timeout) { _syncLoopTimeout = timeout; - connect(this, &Connection::syncDone, this, &Connection::getNewEventsOnSyncDone); - connect(this, &Connection::syncError, this, &Connection::getNewEventsOnSyncError); - _syncLoopElapsedTimer.start(); - sync(_syncLoopTimeout); // initial sync to start the loop + connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); + syncLoopIteration(); // initial sync to start the loop } void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { @@ -420,37 +418,6 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { } } -void Connection::getNewEvents() -{ - int delay = minSyncLoopDelayMs() - _syncLoopElapsedTimer.restart(); - if (delay<0) { - delay = 0; - } - QTimer::singleShot(delay, this, &Connection::syncLoopIteration); -} - -void Connection::getNewEventsOnSyncDone() -{ - if (_prevSyncLoopIterationDone) { - _syncLoopAttemptNumber++; - } else { - _syncLoopAttemptNumber = 0; - } - emit syncAttemptNumberChanged(_syncLoopAttemptNumber); - getNewEvents(); -} - -void Connection::getNewEventsOnSyncError() -{ - if (_prevSyncLoopIterationDone) { - _syncLoopAttemptNumber = 0; - } else { - _syncLoopAttemptNumber++; - } - emit syncAttemptNumberChanged(_syncLoopAttemptNumber); - getNewEvents(); -} - void Connection::stopSync() { if (d->syncJob) @@ -470,15 +437,6 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const return callApi(room->id(), "m.read", event->id()); } -void Connection::setMinSyncDelayMs(qint64 minSyncDelayMs) -{ - if (_minSyncLoopDelayMs == minSyncDelayMs) - return; - - _minSyncLoopDelayMs = minSyncDelayMs; - emit minSyncDelayMsChanged(_minSyncLoopDelayMs); -} - JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { @@ -1143,16 +1101,6 @@ user_factory_t Connection::userFactory() return _userFactory; } -qint64 Connection::minSyncLoopDelayMs() const -{ - return _minSyncLoopDelayMs; -} - -uint Connection::syncLoopAttemptNumber() const -{ - return _syncLoopAttemptNumber; -} - room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); user_factory_t Connection::_userFactory = defaultUserFactory<>(); diff --git a/lib/connection.h b/lib/connection.h index ee7ad243..c9ca3580 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -26,7 +26,6 @@ #include #include #include -#include #include #include @@ -106,8 +105,6 @@ namespace QMatrixClient Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) - Q_PROPERTY(qint64 minSyncLoopDelayMs READ minSyncLoopDelayMs WRITE setMinSyncDelayMs NOTIFY minSyncDelayMsChanged) - Q_PROPERTY(uint syncLoopAttemptNumber READ syncLoopAttemptNumber NOTIFY syncAttemptNumberChanged) public: // Room ids, rather than room pointers, are used in the direct chat @@ -356,11 +353,6 @@ namespace QMatrixClient template static void setUserType() { setUserFactory(defaultUserFactory()); } - qint64 minSyncLoopDelayMs() const; - void setMinSyncDelayMs(qint64 minSyncLoopDelayMs); - - uint syncLoopAttemptNumber() const; - public slots: /** Set the homeserver base URL */ void setHomeserver(const QUrl& baseUrl); @@ -379,13 +371,6 @@ namespace QMatrixClient void logout(); void sync(int timeout = -1); - - /** Start sync loop with the minSyncLoopDelayMs value - where minSyncLoopDelayMs could be changed on the client - according to syncDone/syncError signals and - the syncLoopAttemptNumber counter. - The syncLoopAttemptNumber counter is resetting - after non-repeating syncDone/syncError events*/ void syncLoop(int timeout = -1); void stopSync(); @@ -662,8 +647,6 @@ namespace QMatrixClient void cacheStateChanged(); void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); - void minSyncDelayMsChanged(qint64 minSyncLoopDelayMs); - void syncAttemptNumberChanged(uint syncLoopAttemptNumber); protected: /** @@ -699,9 +682,7 @@ namespace QMatrixClient void onSyncSuccess(SyncData &&data, bool fromCache = false); protected slots: - void getNewEvents(); - void getNewEventsOnSyncDone(); - void getNewEventsOnSyncError(); + void syncLoopIteration(); private: class Private; @@ -727,12 +708,7 @@ namespace QMatrixClient static room_factory_t _roomFactory; static user_factory_t _userFactory; - QElapsedTimer _syncLoopElapsedTimer; int _syncLoopTimeout = -1; - qint64 _minSyncLoopDelayMs = 0; - void syncLoopIteration(); - uint _syncLoopAttemptNumber = 0; - bool _prevSyncLoopIterationDone = false; }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::Connection*) -- cgit v1.2.3 From ae7a982f03e8e57eab014bcd8dd099d9248e63d8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Feb 2019 19:13:55 +0900 Subject: csapi: presence lists are no more --- lib/csapi/presence.cpp | 46 ------------------------------------------- lib/csapi/presence.h | 53 -------------------------------------------------- 2 files changed, 99 deletions(-) diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 8a5510b8..024d7a34 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -83,49 +83,3 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) return Success; } -static const auto ModifyPresenceListJobName = QStringLiteral("ModifyPresenceListJob"); - -ModifyPresenceListJob::ModifyPresenceListJob(const QString& userId, const QStringList& invite, const QStringList& drop) - : BaseJob(HttpVerb::Post, ModifyPresenceListJobName, - basePath % "/presence/list/" % userId) -{ - QJsonObject _data; - addParam(_data, QStringLiteral("invite"), invite); - addParam(_data, QStringLiteral("drop"), drop); - setRequestData(_data); -} - -class GetPresenceForListJob::Private -{ - public: - Events data; -}; - -QUrl GetPresenceForListJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/presence/list/" % userId); -} - -static const auto GetPresenceForListJobName = QStringLiteral("GetPresenceForListJob"); - -GetPresenceForListJob::GetPresenceForListJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetPresenceForListJobName, - basePath % "/presence/list/" % userId, false) - , d(new Private) -{ -} - -GetPresenceForListJob::~GetPresenceForListJob() = default; - -Events&& GetPresenceForListJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetPresenceForListJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index c8f80357..5e132d24 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -6,7 +6,6 @@ #include "jobs/basejob.h" -#include "events/eventloader.h" #include "converters.h" namespace QMatrixClient @@ -74,56 +73,4 @@ namespace QMatrixClient class Private; QScopedPointer d; }; - - /// Add or remove users from this presence list. - /// - /// Adds or removes users from this presence list. - class ModifyPresenceListJob : public BaseJob - { - public: - /*! Add or remove users from this presence list. - * \param userId - * The user whose presence list is being modified. - * \param invite - * A list of user IDs to add to the list. - * \param drop - * A list of user IDs to remove from the list. - */ - explicit ModifyPresenceListJob(const QString& userId, const QStringList& invite = {}, const QStringList& drop = {}); - }; - - /// Get presence events for this presence list. - /// - /// Retrieve a list of presence events for every user on this list. - class GetPresenceForListJob : public BaseJob - { - public: - /*! Get presence events for this presence list. - * \param userId - * The user whose presence list should be retrieved. - */ - explicit GetPresenceForListJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPresenceForListJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetPresenceForListJob() override; - - // Result properties - - /// A list of presence events for this list. - Events&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer d; - }; } // namespace QMatrixClient -- cgit v1.2.3 From c810b069ab827b1149aeeb9e1f662e5ef85867e5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Feb 2019 19:19:35 +0900 Subject: csapi: GetVersionsJob now returns unstableFeatures (MSC1497) --- lib/csapi/versions.cpp | 10 ++++++++++ lib/csapi/versions.h | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index c853ec06..6ee6725d 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -16,6 +16,7 @@ class GetVersionsJob::Private { public: QStringList versions; + QHash unstableFeatures; }; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) @@ -40,10 +41,19 @@ const QStringList& GetVersionsJob::versions() const return d->versions; } +const QHash& GetVersionsJob::unstableFeatures() const +{ + return d->unstableFeatures; +} + BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); + if (!json.contains("versions"_ls)) + return { JsonParseError, + "The key 'versions' not found in the response" }; fromJson(json.value("versions"_ls), d->versions); + fromJson(json.value("unstable_features"_ls), d->unstableFeatures); return Success; } diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 309de184..b56f293f 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -6,6 +6,8 @@ #include "jobs/basejob.h" +#include +#include "converters.h" namespace QMatrixClient { @@ -19,6 +21,19 @@ namespace QMatrixClient /// /// Only the latest ``Z`` value will be reported for each supported ``X.Y`` value. /// i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, it will report ``r0.0.1`` and ``r1.2.0``. + /// + /// The server may additionally advertise experimental features it supports + /// through ``unstable_features``. These features should be namespaced and + /// may optionally include version information within their name if desired. + /// Features listed here are not for optionally toggling parts of the Matrix + /// specification and should only be used to advertise support for a feature + /// which has not yet landed in the spec. For example, a feature currently + /// undergoing the proposal process may appear here and eventually be taken + /// off this list once the feature lands in the spec and the server deems it + /// reasonable to do so. Servers may wish to keep advertising features here + /// after they've been released into the spec to give clients a chance to + /// upgrade appropriately. Additionally, clients should avoid using unstable + /// features in their stable releases. class GetVersionsJob : public BaseJob { public: @@ -38,6 +53,10 @@ namespace QMatrixClient /// The supported versions. const QStringList& versions() const; + /// Experimental features the server supports. Features not listed here, + /// or the lack of this property all together, indicate that a feature is + /// not supported. + const QHash& unstableFeatures() const; protected: Status parseJson(const QJsonDocument& data) override; -- cgit v1.2.3 From d1cf4bc530613a9d3ee10768dd068a0391f6e105 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Feb 2019 19:23:00 +0900 Subject: csapi: GetCapabilitiesJob (MSC1753) --- lib/csapi/capabilities.cpp | 78 ++++++++++++++++++++++++++++++++++++++++++++++ lib/csapi/capabilities.h | 69 ++++++++++++++++++++++++++++++++++++++++ lib/csapi/gtad.yaml | 2 ++ lib/util.h | 3 ++ 4 files changed, 152 insertions(+) create mode 100644 lib/csapi/capabilities.cpp create mode 100644 lib/csapi/capabilities.h diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp new file mode 100644 index 00000000..a8e79f6b --- /dev/null +++ b/lib/csapi/capabilities.cpp @@ -0,0 +1,78 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "capabilities.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +namespace QMatrixClient +{ + // Converters + + template <> struct JsonObjectConverter + { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ChangePasswordCapability& result) + { + fromJson(jo.value("enabled"_ls), result.enabled); + } + }; + + template <> struct JsonObjectConverter + { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result) + { + fromJson(jo.value("default"_ls), result.isDefault); + fromJson(jo.value("available"_ls), result.available); + } + }; +} // namespace QMatrixClient + +class GetCapabilitiesJob::Private +{ + public: + Omittable changePassword; + Omittable roomVersions; +}; + +QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/capabilities"); +} + +static const auto GetCapabilitiesJobName = QStringLiteral("GetCapabilitiesJob"); + +GetCapabilitiesJob::GetCapabilitiesJob() + : BaseJob(HttpVerb::Get, GetCapabilitiesJobName, + basePath % "/capabilities") + , d(new Private) +{ +} + +GetCapabilitiesJob::~GetCapabilitiesJob() = default; + +const Omittable& GetCapabilitiesJob::changePassword() const +{ + return d->changePassword; +} + +const Omittable& GetCapabilitiesJob::roomVersions() const +{ + return d->roomVersions; +} + +BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + fromJson(json.value("m.change_password"_ls), d->changePassword); + fromJson(json.value("m.room_versions"_ls), d->roomVersions); + return Success; +} + diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h new file mode 100644 index 00000000..e38483bc --- /dev/null +++ b/lib/csapi/capabilities.h @@ -0,0 +1,69 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +#include "converters.h" +#include + +namespace QMatrixClient +{ + // Operations + + /// Gets information about the server's capabilities. + /// + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + class GetCapabilitiesJob : public BaseJob + { + public: + // Inner data structures + + /// Capability to indicate if the user can change their password. + struct ChangePasswordCapability + { + /// True if the user can change their password, false otherwise. + bool enabled; + }; + + /// The room versions the server supports. + struct RoomVersionsCapability + { + /// The default room version the server is using for new rooms. + QString isDefault; + /// A detailed description of the room versions the server supports. + QHash available; + }; + + // Construction/destruction + + explicit GetCapabilitiesJob(); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetCapabilitiesJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + ~GetCapabilitiesJob() override; + + // Result properties + + /// Capability to indicate if the user can change their password. + const Omittable& changePassword() const; + /// The room versions the server supports. + const Omittable& roomVersions() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index c6ea8a13..21a59a5c 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -11,6 +11,8 @@ analyzer: m.upload.size: uploadSize m.homeserver: homeserver m.identity_server: identityServer + m.change_password: changePassword + m.room_versions: roomVersions AuthenticationData/additionalProperties: authInfo # Structure inside `types`: diff --git a/lib/util.h b/lib/util.h index ade6e8c2..420b0984 100644 --- a/lib/util.h +++ b/lib/util.h @@ -103,6 +103,9 @@ namespace QMatrixClient } Omittable& operator=(value_type&& val) { + // For some reason GCC complains about -Wmaybe-uninitialized + // in the context of using Omittable with converters.h; + // though the logic looks very much benign (GCC bug???) _value = std::move(val); _omitted = false; return *this; -- cgit v1.2.3 From 0ec97b031c4d89acc9ea6e343620f3762f8eb51b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Feb 2019 19:26:19 +0900 Subject: csapi: UpgradeRoomJob (MSC1501) --- lib/csapi/room_upgrades.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++++ lib/csapi/room_upgrades.h | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 lib/csapi/room_upgrades.cpp create mode 100644 lib/csapi/room_upgrades.h diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp new file mode 100644 index 00000000..f58fd675 --- /dev/null +++ b/lib/csapi/room_upgrades.cpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "room_upgrades.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +class UpgradeRoomJob::Private +{ + public: + QString replacementRoom; +}; + +static const auto UpgradeRoomJobName = QStringLiteral("UpgradeRoomJob"); + +UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) + : BaseJob(HttpVerb::Post, UpgradeRoomJobName, + basePath % "/rooms/" % roomId % "/upgrade") + , d(new Private) +{ + QJsonObject _data; + addParam<>(_data, QStringLiteral("new_version"), newVersion); + setRequestData(_data); +} + +UpgradeRoomJob::~UpgradeRoomJob() = default; + +const QString& UpgradeRoomJob::replacementRoom() const +{ + return d->replacementRoom; +} + +BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("replacement_room"_ls)) + return { JsonParseError, + "The key 'replacement_room' not found in the response" }; + fromJson(json.value("replacement_room"_ls), d->replacementRoom); + return Success; +} + diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h new file mode 100644 index 00000000..6f712f10 --- /dev/null +++ b/lib/csapi/room_upgrades.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + + +namespace QMatrixClient +{ + // Operations + + /// Upgrades a room to a new room version. + /// + /// Upgrades the given room to a particular room version, migrating as much + /// data as possible over to the new room. See the `room_upgrades <#room-upgrades>`_ + /// module for more information on what this entails. + class UpgradeRoomJob : public BaseJob + { + public: + /*! Upgrades a room to a new room version. + * \param roomId + * The ID of the room to upgrade. + * \param newVersion + * The new version for the room. + */ + explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); + ~UpgradeRoomJob() override; + + // Result properties + + /// The ID of the new room. + const QString& replacementRoom() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer d; + }; +} // namespace QMatrixClient -- cgit v1.2.3 From ee1d26586572d4d74105a0713d0237dbc2d183f0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Feb 2019 19:37:12 +0900 Subject: csapi: add RedirectToSSOJob This is actually a rehash (MSC1721) of redirectToCAS that existed before but was explicitly disabled in the library because of its seeming uselessness in the context of non-web clients. On the second thought, however, `RedirectToSSOJob::makeRequestURL()` can actually be used to open a web browser from a non-web client in order to perform the login procedure. --- CMakeLists.txt | 1 - lib/csapi/sso_login_redirect.cpp | 38 ++++++++++++++++++++++++++++++++++++++ lib/csapi/sso_login_redirect.h | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 lib/csapi/sso_login_redirect.cpp create mode 100644 lib/csapi/sso_login_redirect.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f0f8ac5a..9729811b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,7 +120,6 @@ if (MATRIX_DOC_PATH AND GTAD_PATH) add_custom_target(update-api ${ABS_GTAD_PATH} --config ${CSAPI_DIR}/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} - cas_login_redirect.yaml- cas_login_ticket.yaml- old_sync.yaml- room_initial_sync.yaml- # deprecated sync.yaml- # we have a better handcrafted implementation WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/lib diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp new file mode 100644 index 00000000..7323951c --- /dev/null +++ b/lib/csapi/sso_login_redirect.cpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "sso_login_redirect.h" + +#include "converters.h" + +#include + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl) +{ + BaseJob::Query _q; + addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); + return _q; +} + +QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl)); +} + +static const auto RedirectToSSOJobName = QStringLiteral("RedirectToSSOJob"); + +RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) + : BaseJob(HttpVerb::Get, RedirectToSSOJobName, + basePath % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl), + {}, false) +{ +} + diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h new file mode 100644 index 00000000..c09365b0 --- /dev/null +++ b/lib/csapi/sso_login_redirect.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + + +namespace QMatrixClient +{ + // Operations + + /// Redirect the user's browser to the SSO interface. + /// + /// A web-based Matrix client should instruct the user's browser to + /// navigate to this endpoint in order to log in via SSO. + /// + /// The server MUST respond with an HTTP redirect to the SSO interface. + class RedirectToSSOJob : public BaseJob + { + public: + /*! Redirect the user's browser to the SSO interface. + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToSSOJob(const QString& redirectUrl); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * RedirectToSSOJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); + + }; +} // namespace QMatrixClient -- cgit v1.2.3 From 7337876aac42552da6d926b38d7466cf2e51b7d8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 9 Feb 2019 21:02:03 +0900 Subject: csapi: support redirect-after-login (MSC1730) --- lib/csapi/definitions/wellknown/full.cpp | 25 +++++++++++++++++++++ lib/csapi/definitions/wellknown/full.h | 38 ++++++++++++++++++++++++++++++++ lib/csapi/login.cpp | 7 ++++++ lib/csapi/login.h | 6 +++++ lib/csapi/wellknown.cpp | 19 ++++------------ lib/csapi/wellknown.h | 9 +++----- 6 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 lib/csapi/definitions/wellknown/full.cpp create mode 100644 lib/csapi/definitions/wellknown/full.h diff --git a/lib/csapi/definitions/wellknown/full.cpp b/lib/csapi/definitions/wellknown/full.cpp new file mode 100644 index 00000000..5ecef34f --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "full.h" + +using namespace QMatrixClient; + +void JsonObjectConverter::dumpTo( + QJsonObject& jo, const DiscoveryInformation& pod) +{ + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); + addParam(jo, QStringLiteral("m.identity_server"), pod.identityServer); +} + +void JsonObjectConverter::fillFrom( + QJsonObject jo, DiscoveryInformation& result) +{ + fromJson(jo.take("m.homeserver"_ls), result.homeserver); + fromJson(jo.take("m.identity_server"_ls), result.identityServer); + + fromJson(jo, result.additionalProperties); +} + diff --git a/lib/csapi/definitions/wellknown/full.h b/lib/csapi/definitions/wellknown/full.h new file mode 100644 index 00000000..d9346acb --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include +#include "converters.h" +#include "csapi/definitions/wellknown/homeserver.h" +#include "csapi/definitions/wellknown/identity_server.h" +#include + +namespace QMatrixClient +{ + // Data structures + + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + struct DiscoveryInformation + { + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + HomeserverInformation homeserver; + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + Omittable identityServer; + /// Application-dependent keys using Java package naming convention. + QHash additionalProperties; + }; + template <> struct JsonObjectConverter + { + static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod); + static void fillFrom(QJsonObject jo, DiscoveryInformation& pod); + }; + +} // namespace QMatrixClient diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index ee33dac2..5e369b9a 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -67,6 +67,7 @@ class LoginJob::Private QString accessToken; QString homeServer; QString deviceId; + Omittable wellKnown; }; static const auto LoginJobName = QStringLiteral("LoginJob"); @@ -111,6 +112,11 @@ const QString& LoginJob::deviceId() const return d->deviceId; } +const Omittable& LoginJob::wellKnown() const +{ + return d->wellKnown; +} + BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) { auto json = data.object(); @@ -118,6 +124,7 @@ BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) fromJson(json.value("access_token"_ls), d->accessToken); fromJson(json.value("home_server"_ls), d->homeServer); fromJson(json.value("device_id"_ls), d->deviceId); + fromJson(json.value("well_known"_ls), d->wellKnown); return Success; } diff --git a/lib/csapi/login.h b/lib/csapi/login.h index 957d8881..648316df 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -7,6 +7,7 @@ #include "jobs/basejob.h" #include +#include "csapi/definitions/wellknown/full.h" #include "csapi/definitions/user_identifier.h" #include "converters.h" @@ -118,6 +119,11 @@ namespace QMatrixClient /// ID of the logged-in device. Will be the same as the /// corresponding parameter in the request, if one was specified. const QString& deviceId() const; + /// Optional client configuration provided by the server. If present, + /// clients SHOULD use the provided object to reconfigure themselves, + /// optionally validating the URLs within. This object takes the same + /// form as the one returned from .well-known autodiscovery. + const Omittable& wellKnown() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index 97505830..a6107f86 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -15,8 +15,7 @@ static const auto basePath = QStringLiteral("/.well-known"); class GetWellknownJob::Private { public: - HomeserverInformation homeserver; - Omittable identityServer; + DiscoveryInformation data; }; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) @@ -36,24 +35,14 @@ GetWellknownJob::GetWellknownJob() GetWellknownJob::~GetWellknownJob() = default; -const HomeserverInformation& GetWellknownJob::homeserver() const +const DiscoveryInformation& GetWellknownJob::data() const { - return d->homeserver; -} - -const Omittable& GetWellknownJob::identityServer() const -{ - return d->identityServer; + return d->data; } BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) { - auto json = data.object(); - if (!json.contains("m.homeserver"_ls)) - return { JsonParseError, - "The key 'm.homeserver' not found in the response" }; - fromJson(json.value("m.homeserver"_ls), d->homeserver); - fromJson(json.value("m.identity_server"_ls), d->identityServer); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index df4c8c6e..8da9ce9f 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -6,9 +6,8 @@ #include "jobs/basejob.h" +#include "csapi/definitions/wellknown/full.h" #include "converters.h" -#include "csapi/definitions/wellknown/identity_server.h" -#include "csapi/definitions/wellknown/homeserver.h" namespace QMatrixClient { @@ -41,10 +40,8 @@ namespace QMatrixClient // Result properties - /// Information about the homeserver to connect to. - const HomeserverInformation& homeserver() const; - /// Optional. Information about the identity server to connect to. - const Omittable& identityServer() const; + /// Server discovery information. + const DiscoveryInformation& data() const; protected: Status parseJson(const QJsonDocument& data) override; -- cgit v1.2.3 From 73c836239bfa35713ad76d5e205ce2f2dceffffd Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Sun, 10 Feb 2019 15:08:08 +0300 Subject: Connection: move syncLoopTimeout to Connection::Private Signed-off-by: Alexey Andreyev --- lib/connection.cpp | 5 +++-- lib/connection.h | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a9a8bba3..63b0a31d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -90,6 +90,7 @@ class Connection::Private DirectChatUsersMap directChatUsers; std::unordered_map accountData; QString userId; + int syncLoopTimeout = -1; SyncJob* syncJob = nullptr; @@ -232,7 +233,7 @@ void Connection::doConnectToServer(const QString& user, const QString& password, void Connection::syncLoopIteration() { - sync(_syncLoopTimeout); + sync(d->syncLoopTimeout); } void Connection::connectWithToken(const QString& userId, @@ -326,7 +327,7 @@ void Connection::sync(int timeout) void Connection::syncLoop(int timeout) { - _syncLoopTimeout = timeout; + d->syncLoopTimeout = timeout; connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); syncLoopIteration(); // initial sync to start the loop } diff --git a/lib/connection.h b/lib/connection.h index c9ca3580..45b691e1 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -707,8 +707,6 @@ namespace QMatrixClient static room_factory_t _roomFactory; static user_factory_t _userFactory; - - int _syncLoopTimeout = -1; }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::Connection*) -- cgit v1.2.3 From 1e273212eca1dfa294d1a4bb9271261bf5671aa3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 11 Feb 2019 08:05:31 +0900 Subject: SimpleContent: don't derive from Base as it gives zero added value Originally there was an idea to make a common base class for all event content. Aside from really trivial unification of toJson() this doesn't span across various types of events, and since state events use static, rather than dynamic, polymorphism (StateEvent<> is a template with the aggregated content vs. RoomMessageEvent with the aggregated pointer-to-content-base), there's no considerable value in using the base class. If state events start using the same approach as message events, this may be brought back but not until then. --- lib/events/simplestateevents.h | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 5aa24c15..2c23d9ca 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "eventcontent.h" #include "converters.h" @@ -28,7 +27,7 @@ namespace QMatrixClient namespace EventContent { template - class SimpleContent: public Base + class SimpleContent { public: using value_type = T; @@ -39,23 +38,19 @@ namespace QMatrixClient : value(std::forward(value)), key(std::move(keyName)) { } SimpleContent(const QJsonObject& json, QString keyName) - : Base(json) - , value(QMatrixClient::fromJson(json[keyName])) + : value(fromJson(json[keyName])) , key(std::move(keyName)) { } + QJsonObject toJson() const + { + return { { key, QMatrixClient::toJson(value) } }; + } public: T value; protected: QString key; - - private: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert(key, QMatrixClient::toJson(value)); - } }; } // namespace EventContent -- cgit v1.2.3 From e9ace5cbe8a930a8aa3cc81df1a4f73d51c5fa90 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 5 Feb 2019 19:14:55 +0900 Subject: Connection::createRoom: support passing a room version On the path to address #233. --- lib/connection.cpp | 7 ++++--- lib/connection.h | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index c582cf94..88fb547f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -537,7 +537,8 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName, bool isDirect, + QStringList invites, const QString& presetName, + const QString& roomVersion, bool isDirect, const QVector& initialState, const QVector& invite3pids, const QJsonObject& creationContent) @@ -546,7 +547,7 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, auto job = callApi( visibility == PublishRoom ? QStringLiteral("public") : QStringLiteral("private"), - alias, name, topic, invites, invite3pids, QString(/*TODO: #233*/), + alias, name, topic, invites, invite3pids, roomVersion, creationContent, initialState, presetName, isDirect); connect(job, &BaseJob::success, this, [this,job] { emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); @@ -648,7 +649,7 @@ CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", true); + "trusted_private_chat", {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) diff --git a/lib/connection.h b/lib/connection.h index 9e4121f4..9e4c1a26 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -402,7 +402,7 @@ namespace QMatrixClient CreateRoomJob* createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, QStringList invites, const QString& presetName = {}, - bool isDirect = false, + const QString& roomVersion = {}, bool isDirect = false, const QVector& initialState = {}, const QVector& invite3pids = {}, const QJsonObject& creationContent = {}); -- cgit v1.2.3 From 01230c16ef8b529ec07d429617247ee383e5c2bb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 8 Feb 2019 07:18:36 +0900 Subject: RoomCreateEvent Closes #234. --- CMakeLists.txt | 1 + lib/events/roomcreateevent.cpp | 38 +++++++++++++++++++++++++++++++ lib/events/roomcreateevent.h | 51 ++++++++++++++++++++++++++++++++++++++++++ libqmatrixclient.pri | 2 ++ 4 files changed, 92 insertions(+) create mode 100644 lib/events/roomcreateevent.cpp create mode 100644 lib/events/roomcreateevent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9729811b..c7f1012a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ set(libqmatrixclient_SRCS lib/events/roomevent.cpp lib/events/stateevent.cpp lib/events/eventcontent.cpp + lib/events/roomcreateevent.cpp lib/events/roommessageevent.cpp lib/events/roommemberevent.cpp lib/events/typingevent.cpp diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp new file mode 100644 index 00000000..635efb92 --- /dev/null +++ b/lib/events/roomcreateevent.cpp @@ -0,0 +1,38 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* 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 "roomcreateevent.h" + +using namespace QMatrixClient; + +RoomCreateDetails::RoomCreateDetails(const QJsonObject& json) + : federated(fromJson(json["m.federate"_ls])) + , version(fromJson(json["room_version"_ls])) +{ + const auto predecessorJson = json["predecessor"_ls].toObject(); + if (!predecessorJson.isEmpty()) + { + fromJson(predecessorJson["room_id"_ls], predRoomId); + fromJson(predecessorJson["event_id"_ls], predEventId); + } +} + +std::pair RoomCreateEvent::predecessor() const +{ + return { content().predRoomId, content().predEventId }; +} diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h new file mode 100644 index 00000000..d93668f9 --- /dev/null +++ b/lib/events/roomcreateevent.h @@ -0,0 +1,51 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* 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 "stateevent.h" + +namespace QMatrixClient +{ + class RoomCreateDetails + { + public: + explicit RoomCreateDetails(const QJsonObject& json); + + bool federated; + QString version; + QString predRoomId; + QString predEventId; + }; + + class RoomCreateEvent : public StateEvent + { + public: + DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + + explicit RoomCreateEvent(const QJsonObject& obj) + : StateEvent(typeId(), obj) + { } + + bool isFederated() const { return content().federated; } + QString version() const { return content().version; } + std::pair predecessor() const; + bool isUpgrade() const { return !content().predRoomId.isEmpty(); } + }; + REGISTER_EVENT_TYPE(RoomCreateEvent) +} diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index f523f3a2..598a86d6 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -26,6 +26,7 @@ HEADERS += \ $$SRCPATH/events/eventcontent.h \ $$SRCPATH/events/roommessageevent.h \ $$SRCPATH/events/simplestateevents.h \ + $$SRCPATH/events/roomcreateevent.h \ $$SRCPATH/events/roommemberevent.h \ $$SRCPATH/events/roomavatarevent.h \ $$SRCPATH/events/typingevent.h \ @@ -68,6 +69,7 @@ SOURCES += \ $$SRCPATH/events/roomevent.cpp \ $$SRCPATH/events/stateevent.cpp \ $$SRCPATH/events/eventcontent.cpp \ + $$SRCPATH/events/roomcreateevent.cpp \ $$SRCPATH/events/roommessageevent.cpp \ $$SRCPATH/events/roommemberevent.cpp \ $$SRCPATH/events/typingevent.cpp \ -- cgit v1.2.3 From 1c83d54f705ad786e4a27aaab94e3a0af725a07c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 12 Feb 2019 15:58:19 +0900 Subject: Omittable: disallow implicit conversion to value_type altogether Because it works, and fails, in surprising ways. And none of the code uses it, as of now. --- lib/util.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/util.h b/lib/util.h index 420b0984..596872e2 100644 --- a/lib/util.h +++ b/lib/util.h @@ -159,7 +159,6 @@ namespace QMatrixClient } value_type&& release() { _omitted = true; return std::move(_value); } - operator const value_type&() const & { return value(); } const value_type* operator->() const & { return &value(); } value_type* operator->() & { return &editValue(); } const value_type& operator*() const & { return value(); } -- cgit v1.2.3 From 52dcb27e1c19389bd833c93609910483ea3be549 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 12 Feb 2019 07:55:15 +0900 Subject: RoomVersionsCapability: fix naming for 'default' parameter The same word is used as a predicate in push_rule.yaml and as a noun in capabilities.yaml; fortunately, GTAD gives some means to distinguish the two. --- lib/csapi/capabilities.cpp | 2 +- lib/csapi/capabilities.h | 2 +- lib/csapi/gtad.yaml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp index a8e79f6b..bd92cf25 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -28,7 +28,7 @@ namespace QMatrixClient { static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result) { - fromJson(jo.value("default"_ls), result.isDefault); + fromJson(jo.value("default"_ls), result.defaultVersion); fromJson(jo.value("available"_ls), result.available); } }; diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index e38483bc..40a2e6f7 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -33,7 +33,7 @@ namespace QMatrixClient struct RoomVersionsCapability { /// The default room version the server is using for new rooms. - QString isDefault; + QString defaultVersion; /// A detailed description of the room versions the server supports. QHash available; }; diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index 21a59a5c..a44f803a 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -5,7 +5,8 @@ analyzer: identifiers: signed: signedData unsigned: unsignedData - default: isDefault + PushRule/default: isDefault + default: defaultVersion # getCapabilities/RoomVersionsCapability origin_server_ts: originServerTimestamp # Instead of originServerTs start: begin # Because start() is a method in BaseJob m.upload.size: uploadSize -- cgit v1.2.3 From a2d9a7b865bfd93386844270849ab72b36a86fbe Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 12 Feb 2019 16:50:36 +0900 Subject: csapi/capabilities.*: fix the definition As per https://github.com/matrix-org/matrix-doc/pull/1879. --- lib/csapi/capabilities.cpp | 28 +++++++++++++++++----------- lib/csapi/capabilities.h | 21 +++++++++++++++++---- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp index bd92cf25..210423f5 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -32,13 +32,22 @@ namespace QMatrixClient fromJson(jo.value("available"_ls), result.available); } }; + + template <> struct JsonObjectConverter + { + static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result) + { + fromJson(jo.take("m.change_password"_ls), result.changePassword); + fromJson(jo.take("m.room_versions"_ls), result.roomVersions); + fromJson(jo, result.additionalProperties); + } + }; } // namespace QMatrixClient class GetCapabilitiesJob::Private { public: - Omittable changePassword; - Omittable roomVersions; + Capabilities capabilities; }; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) @@ -58,21 +67,18 @@ GetCapabilitiesJob::GetCapabilitiesJob() GetCapabilitiesJob::~GetCapabilitiesJob() = default; -const Omittable& GetCapabilitiesJob::changePassword() const -{ - return d->changePassword; -} - -const Omittable& GetCapabilitiesJob::roomVersions() const +const GetCapabilitiesJob::Capabilities& GetCapabilitiesJob::capabilities() const { - return d->roomVersions; + return d->capabilities; } BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - fromJson(json.value("m.change_password"_ls), d->changePassword); - fromJson(json.value("m.room_versions"_ls), d->roomVersions); + if (!json.contains("capabilities"_ls)) + return { JsonParseError, + "The key 'capabilities' not found in the response" }; + fromJson(json.value("capabilities"_ls), d->capabilities); return Success; } diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index 40a2e6f7..39e2f4d1 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -6,6 +6,7 @@ #include "jobs/basejob.h" +#include #include "converters.h" #include @@ -38,6 +39,19 @@ namespace QMatrixClient QHash available; }; + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + struct Capabilities + { + /// Capability to indicate if the user can change their password. + Omittable changePassword; + /// The room versions the server supports. + Omittable roomVersions; + /// The custom capabilities the server supports, using the + /// Java package naming convention. + QHash additionalProperties; + }; + // Construction/destruction explicit GetCapabilitiesJob(); @@ -54,10 +68,9 @@ namespace QMatrixClient // Result properties - /// Capability to indicate if the user can change their password. - const Omittable& changePassword() const; - /// The room versions the server supports. - const Omittable& roomVersions() const; + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + const Capabilities& capabilities() const; protected: Status parseJson(const QJsonDocument& data) override; -- cgit v1.2.3 From e12fc32b94c3840249676b2e0656c174846f1c6e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 12 Feb 2019 22:11:23 +0900 Subject: Connection: load supported room versions A part of #236. --- lib/connection.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++++++----- lib/connection.h | 8 ++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2a2d4822..6d1763ee 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -24,6 +24,7 @@ #include "room.h" #include "settings.h" #include "csapi/login.h" +#include "csapi/capabilities.h" #include "csapi/logout.h" #include "csapi/receipts.h" #include "csapi/leaving.h" @@ -92,6 +93,9 @@ class Connection::Private QString userId; int syncLoopTimeout = -1; + GetCapabilitiesJob* capabilitiesJob = nullptr; + GetCapabilitiesJob::Capabilities capabilities; + SyncJob* syncJob = nullptr; bool cacheState = true; @@ -244,6 +248,29 @@ void Connection::connectWithToken(const QString& userId, [=] { d->connectWithToken(userId, accessToken, deviceId); }); } +void Connection::reloadCapabilities() +{ + d->capabilitiesJob = callApi(BackgroundRequest); + connect(d->capabilitiesJob, &BaseJob::finished, this, [this] { + if (d->capabilitiesJob->error() == BaseJob::Success) + d->capabilities = d->capabilitiesJob->capabilities(); + else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) + qCDebug(MAIN) << "Server doesn't support /capabilities"; + + if (d->capabilities.roomVersions.omitted()) + { + qCWarning(MAIN) << "Pinning supported room version to 1"; + d->capabilities.roomVersions = { "1", {{ "1", "stable" }} }; + } else { + qCDebug(MAIN) << "Room versions:" + << defaultRoomVersion() << "is default, full list:" + << availableRoomVersions(); + } + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + emit capabilitiesLoaded(); + }); +} + void Connection::Private::connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId) @@ -256,7 +283,7 @@ void Connection::Private::connectWithToken(const QString& user, << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); - + q->reloadCapabilities(); } void Connection::checkAndConnect(const QString& userId, @@ -1259,9 +1286,30 @@ void QMatrixClient::Connection::setLazyLoading(bool newValue) void Connection::getTurnServers() { - auto job = callApi(); - connect( job, &GetTurnServerJob::success, [=] { - emit turnServersChanged(job->data()); - }); + auto job = callApi(); + connect(job, &GetTurnServerJob::success, + this, [=] { emit turnServersChanged(job->data()); }); +} + +QString Connection::defaultRoomVersion() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + return d->capabilities.roomVersions->defaultVersion; +} +QStringList Connection::stableRoomVersions() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + QStringList l; + const auto& allVersions = d->capabilities.roomVersions->available; + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + if (it.value() == "stable") + l.push_back(it.key()); + return l; +} + +const QHash& Connection::availableRoomVersions() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + return d->capabilities.roomVersions->available; } diff --git a/lib/connection.h b/lib/connection.h index 8c938df2..e5bce52e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -102,6 +102,7 @@ namespace QMatrixClient Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) + Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) @@ -257,6 +258,10 @@ namespace QMatrixClient Q_INVOKABLE QString token() const; Q_INVOKABLE void getTurnServers(); + QString defaultRoomVersion() const; + QStringList stableRoomVersions() const; + const QHash& availableRoomVersions() const; + /** * Call this before first sync to load from previously saved file. * @@ -365,6 +370,8 @@ namespace QMatrixClient const QString& deviceId = {}); void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); + /** Explicitly request capabilities from the server */ + void reloadCapabilities(); /** @deprecated Use stopSync() instead */ void disconnectFromServer() { stopSync(); } @@ -501,6 +508,7 @@ namespace QMatrixClient void resolveError(QString error); void homeserverChanged(QUrl baseUrl); + void capabilitiesLoaded(); void connected(); void reconnected(); //< \deprecated Use connected() instead -- cgit v1.2.3 From a8adbc1d3c8ba787321ebd558062a9c12b12324a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 11:10:39 +0900 Subject: RoomTombstoneEvent --- CMakeLists.txt | 1 + lib/events/roomtombstoneevent.cpp | 31 +++++++++++++++++++++++++++++ lib/events/roomtombstoneevent.h | 41 +++++++++++++++++++++++++++++++++++++++ libqmatrixclient.pri | 2 ++ 4 files changed, 75 insertions(+) create mode 100644 lib/events/roomtombstoneevent.cpp create mode 100644 lib/events/roomtombstoneevent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c7f1012a..d2d8c218 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ set(libqmatrixclient_SRCS lib/events/stateevent.cpp lib/events/eventcontent.cpp lib/events/roomcreateevent.cpp + lib/events/roomtombstoneevent.cpp lib/events/roommessageevent.cpp lib/events/roommemberevent.cpp lib/events/typingevent.cpp diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp new file mode 100644 index 00000000..9c3bafd4 --- /dev/null +++ b/lib/events/roomtombstoneevent.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* 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 "roomtombstoneevent.h" + +using namespace QMatrixClient; + +QString RoomTombstoneEvent::serverMessage() const +{ + return fromJson(contentJson()["body"_ls]); +} + +QString RoomTombstoneEvent::successorRoomId() const +{ + return fromJson(contentJson()["replacement_room"_ls]); +} diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h new file mode 100644 index 00000000..c7008ec4 --- /dev/null +++ b/lib/events/roomtombstoneevent.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* 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 "stateevent.h" + +namespace QMatrixClient +{ + class RoomTombstoneEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) + + explicit RoomTombstoneEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomTombstoneEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + QString serverMessage() const; + QString successorRoomId() const; + }; + REGISTER_EVENT_TYPE(RoomTombstoneEvent) +} diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 598a86d6..be568bd2 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -27,6 +27,7 @@ HEADERS += \ $$SRCPATH/events/roommessageevent.h \ $$SRCPATH/events/simplestateevents.h \ $$SRCPATH/events/roomcreateevent.h \ + $$SRCPATH/events/roomtombstoneevent.h \ $$SRCPATH/events/roommemberevent.h \ $$SRCPATH/events/roomavatarevent.h \ $$SRCPATH/events/typingevent.h \ @@ -70,6 +71,7 @@ SOURCES += \ $$SRCPATH/events/stateevent.cpp \ $$SRCPATH/events/eventcontent.cpp \ $$SRCPATH/events/roomcreateevent.cpp \ + $$SRCPATH/events/roomtombstoneevent.cpp \ $$SRCPATH/events/roommessageevent.cpp \ $$SRCPATH/events/roommemberevent.cpp \ $$SRCPATH/events/typingevent.cpp \ -- cgit v1.2.3 From 6af5e93134065cd97644d2eee43b2852df549553 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 11:11:47 +0900 Subject: Simplify RoomCreateEvent --- lib/events/roomcreateevent.cpp | 29 ++++++++++++++++++----------- lib/events/roomcreateevent.h | 32 +++++++++++++++----------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index 635efb92..8fd0f1de 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -20,19 +20,26 @@ using namespace QMatrixClient; -RoomCreateDetails::RoomCreateDetails(const QJsonObject& json) - : federated(fromJson(json["m.federate"_ls])) - , version(fromJson(json["room_version"_ls])) +bool RoomCreateEvent::isFederated() const { - const auto predecessorJson = json["predecessor"_ls].toObject(); - if (!predecessorJson.isEmpty()) - { - fromJson(predecessorJson["room_id"_ls], predRoomId); - fromJson(predecessorJson["event_id"_ls], predEventId); - } + return fromJson(contentJson()["m.federate"_ls]); } -std::pair RoomCreateEvent::predecessor() const +QString RoomCreateEvent::version() const { - return { content().predRoomId, content().predEventId }; + return fromJson(contentJson()["room_version"_ls]); +} + +RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const +{ + const auto predJson = contentJson()["predecessor"_ls].toObject(); + return { + fromJson(predJson["room_id"_ls]), + fromJson(predJson["event_id"_ls]) + }; +} + +bool RoomCreateEvent::isUpgrade() const +{ + return contentJson().contains("predecessor"_ls); } diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index d93668f9..0a8f27cc 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -22,30 +22,28 @@ namespace QMatrixClient { - class RoomCreateDetails - { - public: - explicit RoomCreateDetails(const QJsonObject& json); - - bool federated; - QString version; - QString predRoomId; - QString predEventId; - }; - - class RoomCreateEvent : public StateEvent + class RoomCreateEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + explicit RoomCreateEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } explicit RoomCreateEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) + : StateEventBase(typeId(), obj) { } - bool isFederated() const { return content().federated; } - QString version() const { return content().version; } - std::pair predecessor() const; - bool isUpgrade() const { return !content().predRoomId.isEmpty(); } + struct Predecessor + { + QString roomId; + QString eventId; + }; + + bool isFederated() const; + QString version() const; + Predecessor predecessor() const; + bool isUpgrade() const; }; REGISTER_EVENT_TYPE(RoomCreateEvent) } -- cgit v1.2.3 From 87018c0a180248df4a2f61665efbfb3af84bbfea Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 11:44:29 +0900 Subject: Room::baseStateLoaded Mirroring Connection::loadedRoomState but for each single room (will be used as a NOTIFY signal for one-time-set events). --- lib/room.cpp | 6 ++++++ lib/room.h | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index d806183f..663f6037 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -296,6 +296,12 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name + connectUntil(connection, &Connection::loadedRoomState, this, + [this] (Room* r) { + if (this == r) + emit baseStateLoaded(); + return this == r; // loadedRoomState fires only once per room + }); qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } diff --git a/lib/room.h b/lib/room.h index 029f87b7..7e30a671 100644 --- a/lib/room.h +++ b/lib/room.h @@ -416,6 +416,15 @@ namespace QMatrixClient void markAllMessagesAsRead(); signals: + /// Initial set of state events has been loaded + /** + * The initial set is what comes from the initial sync for the room. + * This includes all basic things like RoomCreateEvent, + * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents + * etc. This is a per-room reflection of Connection::loadedRoomState + * \sa Connection::loadedRoomState + */ + void baseStateLoaded(); void eventsHistoryJobChanged(); void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); -- cgit v1.2.3 From 173cfceab7da61e85467658a2c320609485b1139 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 11:45:23 +0900 Subject: Add a FIXME upon the recent failure under Valgrind --- lib/room.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 663f6037..60c61f2b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1306,6 +1306,8 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); + // FIXME: This sometimes causes a bad read: + // https://travis-ci.org/QMatrixClient/libqmatrixclient/jobs/492156899#L2596 unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); return pEvent; -- cgit v1.2.3 From f3ec748689db531df787d19bcfe76b0a40665b67 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 11:49:40 +0900 Subject: Room: version(), predecessorId(), successorId() Use RoomCreateEvent and RoomTombstoneEvent in the backend, covering most of #235. --- lib/room.cpp | 19 ++++++++++++++++++- lib/room.h | 6 ++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 60c61f2b..e1625478 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -30,6 +30,8 @@ #include "csapi/rooms.h" #include "csapi/tags.h" #include "events/simplestateevents.h" +#include "events/roomcreateevent.h" +#include "events/roomtombstoneevent.h" #include "events/roomavatarevent.h" #include "events/roommemberevent.h" #include "events/typingevent.h" @@ -315,6 +317,21 @@ const QString& Room::id() const return d->id; } +QString Room::version() const +{ + return d->getCurrentState()->version(); +} + +QString Room::predecessorId() const +{ + return d->getCurrentState()->predecessor().roomId; +} + +QString Room::successorId() const +{ + return d->getCurrentState()->successorRoomId(); +} + const Room::Timeline& Room::messageEvents() const { return d->timeline; @@ -1807,7 +1824,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, std::vector> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } -// , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } + , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomPowerLevels::typeId(), // { QStringLiteral("ban"), QStringLiteral("events"), diff --git a/lib/room.h b/lib/room.h index 7e30a671..ef832d1a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -80,6 +80,9 @@ namespace QMatrixClient Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) + Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) + Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) Q_PROPERTY(QString name READ name NOTIFY namesChanged) Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) @@ -143,6 +146,9 @@ namespace QMatrixClient Connection* connection() const; User* localUser() const; const QString& id() const; + QString version() const; + QString predecessorId() const; + QString successorId() const; QString name() const; QStringList aliases() const; QString canonicalAlias() const; -- cgit v1.2.3 From 0f4368a19e344c8e3d74d97d4c9de171e723a9a1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 12:19:38 +0900 Subject: Disallow sending events to rooms that have been upgraded This concludes the mandatory part of #235. --- lib/room.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index e1625478..6b9702cd 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -252,11 +252,17 @@ class Room::Private const QString& txnId, BaseJob* call = nullptr); template - auto requestSetState(const QString& stateKey, const EvT& event) + SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, + const EvT& event) { - // TODO: Queue up state events sending (see #133). - return connection->callApi( + if (q->successorId().isEmpty()) + { + // TODO: Queue up state events sending (see #133). + return connection->callApi( id, EvT::matrixTypeId(), stateKey, event.contentJson()); + } + qCWarning(MAIN) << q << "has been upgraded, state won't be set"; + return nullptr; } template @@ -1332,7 +1338,11 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { - return doSendEvent(addAsPending(std::move(event))); + if (q->successorId().isEmpty()) + return doSendEvent(addAsPending(std::move(event))); + + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; } QString Room::Private::doSendEvent(const RoomEvent* pEvent) -- cgit v1.2.3 From 5ac901775c5ebd39338ae7854d2c3391cf9084fa Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 12:24:41 +0900 Subject: Room::upgraded() A signal emitted when the room receives a tombstone event from the server. --- lib/room.cpp | 3 +++ lib/room.h | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 6b9702cd..af97dc11 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2160,6 +2160,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit encryption(); // It can only be done once, so emit it here. return EncryptionOn; } + , [this] (const RoomTombstoneEvent& evt) { + emit upgraded(evt.serverMessage(), evt.successorRoomId()); + } ); } diff --git a/lib/room.h b/lib/room.h index ef832d1a..137b383d 100644 --- a/lib/room.h +++ b/lib/room.h @@ -528,6 +528,10 @@ namespace QMatrixClient void fileTransferCancelled(QString id); void callEvent(Room* room, const RoomEvent* event); + + /// This room has been upgraded and won't receive updates anymore + void upgraded(QString serverMessage, QString successorId); + /// The room is about to be deleted void beforeDestruction(Room*); -- cgit v1.2.3 From ac7d2ad8b0942cc465c0d340f159cb0b343008ab Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 12:29:02 +0900 Subject: Room::checkVersion() and Room::unstableVersion() Initial (sans power levels checking) implementation of the check that room should be upgraded. Closes most of #236. --- lib/connection.cpp | 9 +++++++++ lib/room.cpp | 17 ++++++++++++++++- lib/room.h | 6 ++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 6d1763ee..22fa2f15 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -268,6 +268,9 @@ void Connection::reloadCapabilities() } Q_ASSERT(!d->capabilities.roomVersions.omitted()); emit capabilitiesLoaded(); + for (auto* r: d->roomMap) + if (r->joinState() == JoinState::Join && r->successorId().isEmpty()) + r->checkVersion(); }); } @@ -383,8 +386,14 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->pendingStateRoomIds.removeOne(roomData.roomId); r->updateData(std::move(roomData), fromCache); if (d->firstTimeRooms.removeOne(r)) + { emit loadedRoomState(r); + if (!d->capabilities.roomVersions.omitted()) + r->checkVersion(); + // Otherwise, the version will be checked in reloadCapabilities() + } } + // Let UI update itself after updating each room QCoreApplication::processEvents(); } for (auto&& accountEvent: data.takeAccountData()) diff --git a/lib/room.cpp b/lib/room.cpp index af97dc11..580d04b8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1604,7 +1604,22 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) bool Room::supportsCalls() const { - return joinedCount() == 2; + return joinedCount() == 2; +} + +void Room::checkVersion() +{ + const auto defaultVersion = connection()->defaultRoomVersion(); + const auto stableVersions = connection()->stableRoomVersions(); + Q_ASSERT(!defaultVersion.isEmpty() && successorId().isEmpty()); + if (!stableVersions.contains(version())) + { + qCDebug(MAIN) << this << "version is" << version() + << "which the server doesn't count as stable"; + // TODO: m.room.power_levels + qCDebug(MAIN) << "The current user has enough privileges to fix it"; + emit unstableVersion(defaultVersion, stableVersions); + } } void Room::inviteCall(const QString& callId, const int lifetime, diff --git a/lib/room.h b/lib/room.h index 137b383d..246206d4 100644 --- a/lib/room.h +++ b/lib/room.h @@ -377,6 +377,9 @@ namespace QMatrixClient Q_INVOKABLE bool supportsCalls() const; public slots: + /** Check whether the room should be upgraded */ + void checkVersion(); + QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); QString postHtmlMessage(const QString& plainText, @@ -529,6 +532,9 @@ namespace QMatrixClient void callEvent(Room* room, const RoomEvent* event); + /// The room's version is considered unstable; upgrade recommended + void unstableVersion(QString recommendedDefault, + QStringList stableVersions); /// This room has been upgraded and won't receive updates anymore void upgraded(QString serverMessage, QString successorId); -- cgit v1.2.3 From 5460bf4024999b78fb3837ffc14ca818a71dd4dc Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 15:45:18 +0900 Subject: Use Changes enum properly Don't use distinct items for each type of event; only for repeated/ combinable ones. --- lib/room.cpp | 3 ++- lib/room.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 580d04b8..23fb19db 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2173,10 +2173,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) } , [this] (const EncryptionEvent&) { emit encryption(); // It can only be done once, so emit it here. - return EncryptionOn; + return OtherChange; } , [this] (const RoomTombstoneEvent& evt) { emit upgraded(evt.serverMessage(), evt.successorRoomId()); + return OtherChange; } ); } diff --git a/lib/room.h b/lib/room.h index 246206d4..0569a0c0 100644 --- a/lib/room.h +++ b/lib/room.h @@ -128,7 +128,7 @@ namespace QMatrixClient JoinStateChange = 0x20, TagsChange = 0x40, MembersChange = 0x80, - EncryptionOn = 0x100, + /*blank*/ = 0x100, AccountDataChange = 0x200, SummaryChange = 0x400, ReadMarkerChange = 0x800, -- cgit v1.2.3 From 0130d9646af5530180158854dbedc35d7c01fd4f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 15 Feb 2019 16:46:53 +0900 Subject: Fix FTBFS --- lib/room.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.h b/lib/room.h index 0569a0c0..0636ba17 100644 --- a/lib/room.h +++ b/lib/room.h @@ -128,7 +128,7 @@ namespace QMatrixClient JoinStateChange = 0x20, TagsChange = 0x40, MembersChange = 0x80, - /*blank*/ = 0x100, + /* = 0x100, */ AccountDataChange = 0x200, SummaryChange = 0x400, ReadMarkerChange = 0x800, -- cgit v1.2.3 From 11b1bfe8f3640bfb1e2dd1710624c67aedb4f98b Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 16 Feb 2019 17:27:39 +0900 Subject: Room::switchVersion() Closes #236. --- lib/room.cpp | 6 ++++++ lib/room.h | 3 +++ 2 files changed, 9 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 23fb19db..aa835860 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -29,6 +29,7 @@ #include "csapi/room_send.h" #include "csapi/rooms.h" #include "csapi/tags.h" +#include "csapi/room_upgrades.h" #include "events/simplestateevents.h" #include "events/roomcreateevent.h" #include "events/roomtombstoneevent.h" @@ -791,6 +792,11 @@ void Room::resetHighlightCount() emit highlightCountChanged(this); } +void Room::switchVersion(QString newVersion) +{ + connection()->callApi(id(), newVersion); +} + bool Room::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.end(); diff --git a/lib/room.h b/lib/room.h index 0636ba17..e09da94c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -424,6 +424,9 @@ namespace QMatrixClient /// Mark all messages in the room as read void markAllMessagesAsRead(); + /// Switch the room's version (aka upgrade) + void switchVersion(QString newVersion); + signals: /// Initial set of state events has been loaded /** -- cgit v1.2.3 From 4e2de22c7d327836d2fe44764f8c7855a51f6206 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 16 Feb 2019 17:29:15 +0900 Subject: Room::checkVersion(): check power levels This is a flimsy implementation without proper RoomPowerLevelEvent definition, just to enable upgrades without causing noise to each and every user of a room on an unstable version. --- lib/room.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index aa835860..3a37053d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1622,9 +1622,25 @@ void Room::checkVersion() { qCDebug(MAIN) << this << "version is" << version() << "which the server doesn't count as stable"; - // TODO: m.room.power_levels - qCDebug(MAIN) << "The current user has enough privileges to fix it"; - emit unstableVersion(defaultVersion, stableVersions); + // TODO, #276: m.room.power_levels + if (const auto* plEvt = + d->currentState.value({"m.room.power_levels", ""})) + { + const auto plJson = plEvt->contentJson(); + const auto currentUserLevel = + plJson.value("users"_ls).toObject() + .value(localUser()->id()).toInt( + plJson.value("users_default"_ls).toInt()); + const auto tombstonePowerLevel = + plJson.value("events").toObject() + .value("m.room.tombstone"_ls).toInt( + plJson.value("state_default"_ls).toInt()); + if (currentUserLevel >= tombstonePowerLevel) + { + qCDebug(MAIN) << "The current user has enough privileges to fix it"; + emit unstableVersion(defaultVersion, stableVersions); + } + } } } -- cgit v1.2.3 From 7e9bf3911e0457bf5af21672d4325882584b78ad Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 16 Feb 2019 20:00:46 +0900 Subject: Room::canSwitchVersions() --- lib/room.cpp | 40 +++++++++++++++++++++++----------------- lib/room.h | 3 +++ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 3a37053d..538c1562 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -573,6 +573,26 @@ void Room::markAllMessagesAsRead() d->markMessagesAsRead(d->timeline.crbegin()); } +bool Room::canSwitchVersions() const +{ + // TODO, #276: m.room.power_levels + const auto* plEvt = + d->currentState.value({"m.room.power_levels", ""}); + if (!plEvt) + return true; + + const auto plJson = plEvt->contentJson(); + const auto currentUserLevel = + plJson.value("users"_ls).toObject() + .value(localUser()->id()).toInt( + plJson.value("users_default"_ls).toInt()); + const auto tombstonePowerLevel = + plJson.value("events").toObject() + .value("m.room.tombstone"_ls).toInt( + plJson.value("state_default"_ls).toInt()); + return currentUserLevel >= tombstonePowerLevel; +} + bool Room::hasUnreadMessages() const { return unreadCount() >= 0; @@ -1622,24 +1642,10 @@ void Room::checkVersion() { qCDebug(MAIN) << this << "version is" << version() << "which the server doesn't count as stable"; - // TODO, #276: m.room.power_levels - if (const auto* plEvt = - d->currentState.value({"m.room.power_levels", ""})) + if (canSwitchVersions()) { - const auto plJson = plEvt->contentJson(); - const auto currentUserLevel = - plJson.value("users"_ls).toObject() - .value(localUser()->id()).toInt( - plJson.value("users_default"_ls).toInt()); - const auto tombstonePowerLevel = - plJson.value("events").toObject() - .value("m.room.tombstone"_ls).toInt( - plJson.value("state_default"_ls).toInt()); - if (currentUserLevel >= tombstonePowerLevel) - { - qCDebug(MAIN) << "The current user has enough privileges to fix it"; - emit unstableVersion(defaultVersion, stableVersions); - } + qCDebug(MAIN) << "The current user has enough privileges to fix it"; + emit unstableVersion(defaultVersion, stableVersions); } } } diff --git a/lib/room.h b/lib/room.h index e09da94c..f12627f3 100644 --- a/lib/room.h +++ b/lib/room.h @@ -424,6 +424,9 @@ namespace QMatrixClient /// Mark all messages in the room as read void markAllMessagesAsRead(); + /// Whether the current user is allowed to upgrade the room + bool canSwitchVersions() const; + /// Switch the room's version (aka upgrade) void switchVersion(QString newVersion); -- cgit v1.2.3 From 73e6bd47f4bafa7e65f8d826d8c6527c59aeb865 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 16 Feb 2019 20:22:01 +0900 Subject: Room::version(): Fallback an empty version to "1" --- lib/room.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 538c1562..14e60f51 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -326,7 +326,8 @@ const QString& Room::id() const QString Room::version() const { - return d->getCurrentState()->version(); + const auto v = d->getCurrentState()->version(); + return v.isEmpty() ? "1" : v; } QString Room::predecessorId() const -- cgit v1.2.3 From ac5daf2ed495a932aba23606f5b3d0dca5aaf676 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 17 Feb 2019 17:41:47 +0900 Subject: Connection: loadingCapabilities(); sort availableRoomVersions --- lib/connection.cpp | 37 ++++++++++++++++++++++++++++++++++--- lib/connection.h | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 22fa2f15..4c0fe6b8 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -274,6 +274,13 @@ void Connection::reloadCapabilities() }); } +bool Connection::loadingCapabilities() const +{ + // (Ab)use the fact that room versions cannot be omitted after + // the capabilities have been loaded (see reloadCapabilities() above). + return d->capabilities.roomVersions.omitted(); +} + void Connection::Private::connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId) @@ -1300,6 +1307,9 @@ void Connection::getTurnServers() this, [=] { emit turnServersChanged(job->data()); }); } +const QString Connection::SupportedRoomVersion::StableTag = + QStringLiteral("stable"); + QString Connection::defaultRoomVersion() const { Q_ASSERT(!d->capabilities.roomVersions.omitted()); @@ -1312,13 +1322,34 @@ QStringList Connection::stableRoomVersions() const QStringList l; const auto& allVersions = d->capabilities.roomVersions->available; for (auto it = allVersions.begin(); it != allVersions.end(); ++it) - if (it.value() == "stable") + if (it.value() == SupportedRoomVersion::StableTag) l.push_back(it.key()); return l; } -const QHash& Connection::availableRoomVersions() const +inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, + const Connection::SupportedRoomVersion& v2) +{ + bool ok1 = false, ok2 = false; + const auto vNum1 = v1.id.toFloat(&ok1); + const auto vNum2 = v2.id.toFloat(&ok2); + return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id; +} + +QVector Connection::availableRoomVersions() const { Q_ASSERT(!d->capabilities.roomVersions.omitted()); - return d->capabilities.roomVersions->available; + QVector result; + result.reserve(d->capabilities.roomVersions->available.size()); + for (auto it = d->capabilities.roomVersions->available.begin(); + it != d->capabilities.roomVersions->available.end(); ++it) + result.push_back({ it.key(), it.value() }); + // Put stable versions over unstable; within each group, + // sort numeric versions as numbers, the rest as strings. + const auto mid = std::partition(result.begin(), result.end(), + std::mem_fn(&SupportedRoomVersion::isStable)); + std::sort(result.begin(), mid, roomVersionLess); + std::sort(mid, result.end(), roomVersionLess); + + return result; } diff --git a/lib/connection.h b/lib/connection.h index e5bce52e..1faee255 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -258,9 +258,43 @@ namespace QMatrixClient Q_INVOKABLE QString token() const; Q_INVOKABLE void getTurnServers(); + struct SupportedRoomVersion + { + QString id; + QString status; + + static const QString StableTag; // "stable", as of CS API 0.5 + bool isStable() const { return status == StableTag; } + + // Pretty-printing + + friend QDebug operator<<(QDebug dbg, + const SupportedRoomVersion& v) + { + QDebugStateSaver _(dbg); + return dbg.nospace() << v.id << '/' << v.status; + } + + friend QDebug operator<<(QDebug dbg, + const QVector& vs) + { + return QtPrivate::printSequentialContainer( + dbg, "", vs); + } + }; + + /// Get the room version recommended by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ QString defaultRoomVersion() const; + /// Get the room version considered stable by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ QStringList stableRoomVersions() const; - const QHash& availableRoomVersions() const; + /// Get all room versions supported by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QVector availableRoomVersions() const; /** * Call this before first sync to load from previously saved file. @@ -370,9 +404,12 @@ namespace QMatrixClient const QString& deviceId = {}); void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); - /** Explicitly request capabilities from the server */ + /// Explicitly request capabilities from the server void reloadCapabilities(); + /// Find out if capabilites are still loading from the server + bool loadingCapabilities() const; + /** @deprecated Use stopSync() instead */ void disconnectFromServer() { stopSync(); } void logout(); -- cgit v1.2.3 From 061c6a69fd55696e7dd82854ace9aa67915628d7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 17 Feb 2019 17:46:26 +0900 Subject: Room: emit room, not id in upgraded(); add upgradeFailed() --- lib/room.cpp | 19 +++++++++++++++++-- lib/room.h | 4 +++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 14e60f51..9c923de7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -815,7 +815,10 @@ void Room::resetHighlightCount() void Room::switchVersion(QString newVersion) { - connection()->callApi(id(), newVersion); + auto* job = connection()->callApi(id(), newVersion); + connect(job, &BaseJob::failure, this, [this,job] { + emit upgradeFailed(job->errorString()); + }); } bool Room::hasAccountData(const QString& type) const @@ -2205,7 +2208,19 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return OtherChange; } , [this] (const RoomTombstoneEvent& evt) { - emit upgraded(evt.serverMessage(), evt.successorRoomId()); + const auto newRoomId = evt.successorRoomId(); + if (auto* newRoom = connection()->room(newRoomId)) + emit upgraded(evt.serverMessage(), newRoom); + else + connectUntil(connection(), &Connection::loadedRoomState, this, + [this,newRoomId,serverMsg=evt.serverMessage()] + (Room* newRoom) { + if (newRoom->id() != newRoomId) + return false; + emit upgraded(serverMsg, newRoom); + return true; + }); + return OtherChange; } ); diff --git a/lib/room.h b/lib/room.h index f12627f3..933a8dd9 100644 --- a/lib/room.h +++ b/lib/room.h @@ -542,7 +542,9 @@ namespace QMatrixClient void unstableVersion(QString recommendedDefault, QStringList stableVersions); /// This room has been upgraded and won't receive updates anymore - void upgraded(QString serverMessage, QString successorId); + void upgraded(QString serverMessage, Room* successor); + /// An attempted room upgrade has failed + void upgradeFailed(QString errorMessage); /// The room is about to be deleted void beforeDestruction(Room*); -- cgit v1.2.3 From ad5d44f31b3ab7e582b84ab05161c97cbc7eefc8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 17 Feb 2019 18:54:52 +0900 Subject: Room: add isUnstable(); unstableVersion() -> stabilityUpdated() --- lib/room.cpp | 12 +++++++++--- lib/room.h | 8 +++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 9c923de7..b13e7873 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -330,6 +330,12 @@ QString Room::version() const return v.isEmpty() ? "1" : v; } +bool Room::isUnstable() const +{ + return !connection()->loadingCapabilities() && + !connection()->stableRoomVersions().contains(version()); +} + QString Room::predecessorId() const { return d->getCurrentState()->predecessor().roomId; @@ -1642,15 +1648,15 @@ void Room::checkVersion() const auto defaultVersion = connection()->defaultRoomVersion(); const auto stableVersions = connection()->stableRoomVersions(); Q_ASSERT(!defaultVersion.isEmpty() && successorId().isEmpty()); + // This method is only called after the base state has been loaded + // or the server capabilities have been loaded. + emit stabilityUpdated(defaultVersion, stableVersions); if (!stableVersions.contains(version())) { qCDebug(MAIN) << this << "version is" << version() << "which the server doesn't count as stable"; if (canSwitchVersions()) - { qCDebug(MAIN) << "The current user has enough privileges to fix it"; - emit unstableVersion(defaultVersion, stableVersions); - } } } diff --git a/lib/room.h b/lib/room.h index 933a8dd9..197926e7 100644 --- a/lib/room.h +++ b/lib/room.h @@ -81,6 +81,7 @@ namespace QMatrixClient Q_PROPERTY(User* localUser READ localUser CONSTANT) Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) + Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) Q_PROPERTY(QString name READ name NOTIFY namesChanged) @@ -147,6 +148,7 @@ namespace QMatrixClient User* localUser() const; const QString& id() const; QString version() const; + bool isUnstable() const; QString predecessorId() const; QString successorId() const; QString name() const; @@ -538,9 +540,9 @@ namespace QMatrixClient void callEvent(Room* room, const RoomEvent* event); - /// The room's version is considered unstable; upgrade recommended - void unstableVersion(QString recommendedDefault, - QStringList stableVersions); + /// The room's version stability may have changed + void stabilityUpdated(QString recommendedDefault, + QStringList stableVersions); /// This room has been upgraded and won't receive updates anymore void upgraded(QString serverMessage, Room* successor); /// An attempted room upgrade has failed -- cgit v1.2.3 From 9ef28a3b43dc576716ace005e300b43c3af74b9f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 18 Feb 2019 07:00:28 +0900 Subject: Room: fix building with MSVC --- lib/room.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index b13e7873..f6956d82 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2214,14 +2214,14 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return OtherChange; } , [this] (const RoomTombstoneEvent& evt) { - const auto newRoomId = evt.successorRoomId(); - if (auto* newRoom = connection()->room(newRoomId)) - emit upgraded(evt.serverMessage(), newRoom); + const auto successorId = evt.successorRoomId(); + if (auto* successor = connection()->room(successorId)) + emit upgraded(evt.serverMessage(), successor); else connectUntil(connection(), &Connection::loadedRoomState, this, - [this,newRoomId,serverMsg=evt.serverMessage()] + [this,successorId,serverMsg=evt.serverMessage()] (Room* newRoom) { - if (newRoom->id() != newRoomId) + if (newRoom->id() != successorId) return false; emit upgraded(serverMsg, newRoom); return true; -- cgit v1.2.3 From 6edd8a23765285350a667ae214f5e450f5f24129 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 19 Feb 2019 21:12:15 +0900 Subject: Room::downloadFile: construct the temporary filename more carefully Closes #279. --- lib/room.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index f6956d82..c6376a26 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -56,6 +56,7 @@ #include #include #include +#include #include #include @@ -1804,7 +1805,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) { // Build our own file path, starting with temp directory and eventId. filePath = eventId; - filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') % + filePath = QDir::tempPath() % '/' % + filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") % '#' % d->fileNameToDownload(event); } auto job = connection()->downloadFile(fileUrl, filePath); -- cgit v1.2.3 From 20e4f76280a8ac36ca1cdfe7d0d7bdeb2eef444d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Feb 2019 17:48:39 +0900 Subject: BaseJob: M_UNSUPPORTED_ROOM_VERSION & M_INCOMPATIBLE_ROOM_VERSION --- lib/jobs/basejob.cpp | 12 +++++++++++- lib/jobs/basejob.h | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 4a7780b1..628d10ec 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -325,7 +325,15 @@ void BaseJob::gotReply() d->status.code = UserConsentRequiredError; d->errorUrl = json.value("consent_uri"_ls).toString(); } - else if (!json.isEmpty()) // Not localisable on the client side + else if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || + errCode == "M_INCOMPATIBLE_ROOM_VERSION") + { + d->status.code = UnsupportedRoomVersionError; + if (json.contains("room_version")) + d->status.message = + tr("Requested room version: %1") + .arg(json.value("room_version").toString()); + } else if (!json.isEmpty()) // Not localisable on the client side setStatus(IncorrectRequestError, json.value("error"_ls).toString()); } @@ -568,6 +576,8 @@ QString BaseJob::statusCaption() const return tr("Network authentication required"); case UserConsentRequiredError: return tr("User consent required"); + case UnsupportedRoomVersionError: + return tr("The server does not support the needed room version"); default: return tr("Request failed"); } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index dd6f9fc8..4c1c7706 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -64,6 +64,7 @@ namespace QMatrixClient , IncorrectResponseError , TooManyRequestsError , RequestNotImplementedError + , UnsupportedRoomVersionError , NetworkAuthRequiredError , UserConsentRequiredError , UserDefinedError = 200 -- cgit v1.2.3 From 3e69bcc053a66c385c2c0ad9e6ae2e36eefaf4e3 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 20 Feb 2019 19:56:55 +0900 Subject: .travis.yml: minor improvements --- .travis.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6926f4ed..515c6c50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,6 @@ addons: - qt57base - qt57multimedia - valgrind - homebrew: - packages: - - qt5 matrix: include: @@ -24,7 +21,11 @@ matrix: compiler: clang - os: osx osx_image: xcode10 - env: [ 'ENV_EVAL="PATH=/usr/local/opt/qt/bin:$PATH"' ] + env: [ 'PATH=/usr/local/opt/qt/bin:$PATH' ] + addons: + homebrew: + packages: + - qt5 before_install: - eval "${ENV_EVAL}" -- cgit v1.2.3 From e48c5db65cb078c8ee84fd617441a78247671dad Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 21 Feb 2019 07:27:59 +0900 Subject: Travis CI: switch macOS builds to xcode10.1 image xcode10.0 seems to have Homebrew broken, and xcode9.4 has a problem with SSLSetALPNProtocols (see also commit f545d181ade8736dfda93e8abb34ab93ac34e931). --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 515c6c50..3aaa4039 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: - os: linux compiler: clang - os: osx - osx_image: xcode10 + osx_image: xcode10.1 env: [ 'PATH=/usr/local/opt/qt/bin:$PATH' ] addons: homebrew: -- cgit v1.2.3 From 297216e95c0802248110403f1b8fdcd5eb02fae6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 1 Feb 2019 07:30:09 +0900 Subject: Room::setAliases, Connection: roomByAlias, updateRoomAliases --- lib/connection.cpp | 37 +++++++++++++++++++++++++++++++ lib/connection.h | 16 +++++++++++--- lib/room.cpp | 64 ++++++++++++++++++++++++++++++++++-------------------- lib/room.h | 1 + 4 files changed, 91 insertions(+), 27 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 4c0fe6b8..998282d3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -83,6 +83,8 @@ class Connection::Private // separately so we should, e.g., keep objects for Invite and // Leave state of the same room. QHash, Room*> roomMap; + // Mapping from aliases to room ids, as per the last sync + QHash roomAliasMap; QVector roomIdsToForget; QVector firstTimeRooms; QVector pendingStateRoomIds; @@ -802,6 +804,41 @@ Room* Connection::room(const QString& roomId, JoinStates states) const return nullptr; } +Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const +{ + const auto id = d->roomAliasMap.value(roomAlias); + if (!id.isEmpty()) + return room(id, states); + qCWarning(MAIN) << "Room for alias" << roomAlias + << "is not found under account" << userId(); + return nullptr; +} + +void Connection::updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases) +{ + for (const auto& a: previousRoomAliases) + if (d->roomAliasMap.remove(a) == 0) + qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; + + for (const auto& a: roomAliases) + { + auto& mappedId = d->roomAliasMap[a]; + if (!mappedId.isEmpty()) + { + if (mappedId == roomId) + qCDebug(MAIN) << "Alias" << a << "is already mapped to room" + << roomId; + else + qCWarning(MAIN) << "Alias" << a + << "will be force-remapped from room" + << mappedId << "to" << roomId; + } + mappedId = roomId; + } +} + Room* Connection::invitation(const QString& roomId) const { return d->roomMap.value({roomId, true}, nullptr); diff --git a/lib/connection.h b/lib/connection.h index 1faee255..49039ffe 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -231,8 +231,8 @@ namespace QMatrixClient */ void addToIgnoredUsers(const User* user); - /** Remove the user from the ignore list - * Similar to adding, the change signal is emitted synchronously. + /** Remove the user from the ignore list */ + /** Similar to adding, the change signal is emitted synchronously. * * \sa ignoredUsersListChanged */ @@ -242,8 +242,18 @@ namespace QMatrixClient QMap users() const; QUrl homeserver() const; + /** Find a room by its id and a mask of applicable states */ Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite|JoinState::Join) const; + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Find a room by its alias and a mask of applicable states */ + Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Update the internal map of room aliases to IDs */ + /// This is used for internal bookkeeping of rooms. Do NOT use + /// it to try change aliases, use Room::setAliases instead + void updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; Q_INVOKABLE User* user(const QString& userId); const User* user() const; diff --git a/lib/room.cpp b/lib/room.cpp index c6376a26..f9c899cb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -209,6 +209,28 @@ class Room::Private is(*ti); } + template + Changes updateStateFrom(EventArrayT&& events) + { + Changes changes = NoChange; + if (!events.empty()) + { + QElapsedTimer et; et.start(); + for (auto&& eptr: events) + { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + // Update baseState afterwards to make sure that the old state + // is valid and usable inside processStateEvent + changes |= q->processStateEvent(evt); + baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); + } + if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" + << events.size() << "event(s)," << et; + } + return changes; + } Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); @@ -684,13 +706,7 @@ void Room::Private::getAllMembers() auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; connect( allMembersJob, &BaseJob::success, q, [=] { Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); - Changes roomChanges = NoChange; - for (auto&& e: allMembersJob->chunk()) - { - const auto& evt = *e; - baseState[{evt.matrixType(),evt.stateKey()}] = move(e); - roomChanges |= q->processStateEvent(evt); - } + auto roomChanges = updateStateFrom(allMembersJob->chunk()); // Replay member events that arrived after the point for which // the full members list was requested. if (!timeline.empty() ) @@ -1296,21 +1312,8 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) for (auto&& event: data.accountData) roomChanges |= processAccountDataEvent(move(event)); - if (!data.state.empty()) - { - et.restart(); - for (auto&& eptr: data.state) - { - const auto& evt = *eptr; - Q_ASSERT(evt.isStateEvent()); - d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); - roomChanges |= processStateEvent(evt); - } + roomChanges |= d->updateStateFrom(data.state); - if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; - } if (!data.timeline.empty()) { et.restart(); @@ -1614,6 +1617,11 @@ void Room::setCanonicalAlias(const QString& newAlias) d->requestSetState(RoomCanonicalAliasEvent(newAlias)); } +void Room::setAliases(const QStringList& aliases) +{ + d->requestSetState(RoomAliasesEvent(aliases)); +} + void Room::setTopic(const QString& newTopic) { d->requestSetState(RoomTopicEvent(newTopic)); @@ -2148,8 +2156,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!e.isStateEvent()) return Change::NoChange; - d->currentState[{e.matrixType(),e.stateKey()}] = - static_cast(&e); + const auto* oldStateEvent = std::exchange( + d->currentState[{e.matrixType(),e.stateKey()}], + static_cast(&e)); + Q_ASSERT(!oldStateEvent || + (oldStateEvent->matrixType() == e.matrixType() && + oldStateEvent->stateKey() == e.stateKey())); if (!is(e)) qCDebug(EVENTS) << "Room state event:" << e; @@ -2157,7 +2169,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) , [] (const RoomNameEvent&) { return NameChange; } - , [] (const RoomAliasesEvent&) { + , [this,oldStateEvent] (const RoomAliasesEvent& ae) { + const auto previousAliases = oldStateEvent + ? static_cast(oldStateEvent)->aliases() + : QStringList(); + connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); return OtherChange; } , [this] (const RoomCanonicalAliasEvent& evt) { diff --git a/lib/room.h b/lib/room.h index 197926e7..808c6074 100644 --- a/lib/room.h +++ b/lib/room.h @@ -402,6 +402,7 @@ namespace QMatrixClient void discardMessage(const QString& txnId); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setAliases(const QStringList& aliases); void setTopic(const QString& newTopic); void getPreviousContent(int limit = 10); -- cgit v1.2.3 From 9931e90193d4a233893540ac5534fa46d7a8d006 Mon Sep 17 00:00:00 2001 From: Alexander Akulich Date: Sat, 23 Feb 2019 13:59:46 +0300 Subject: Remove the 'pretty' SupportedRoomVersion vector debug operator This way we conform with Qt standard debug output and do not rely on a Qt private API. This also fixes compilation for Qt < 5.7. --- lib/connection.h | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/connection.h b/lib/connection.h index 1faee255..1a81c29e 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -266,21 +266,12 @@ namespace QMatrixClient static const QString StableTag; // "stable", as of CS API 0.5 bool isStable() const { return status == StableTag; } - // Pretty-printing - friend QDebug operator<<(QDebug dbg, const SupportedRoomVersion& v) { QDebugStateSaver _(dbg); return dbg.nospace() << v.id << '/' << v.status; } - - friend QDebug operator<<(QDebug dbg, - const QVector& vs) - { - return QtPrivate::printSequentialContainer( - dbg, "", vs); - } }; /// Get the room version recommended by the server -- cgit v1.2.3 From 1a9fd422581cf14c384d2467950ab3f2e1039565 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Sun, 24 Feb 2019 12:45:16 +0300 Subject: Fix Qt<5.7 build for std::hash --- lib/events/stateevent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index dc017b11..3f54f7bf 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -46,7 +46,7 @@ namespace QMatrixClient { * of state in Matrix. * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events */ - using StateEventKey = std::pair; + using StateEventKey = QPair; template struct Prev -- cgit v1.2.3 From 6636e46a2e9049f261b8a64cb6c1bf7c4f076c54 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 25 Feb 2019 11:17:34 +0900 Subject: makeRedacted: update the list of preserved parts Closes #256. --- lib/room.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index f9c899cb..7e7d8505 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1889,22 +1889,29 @@ RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { auto originalJson = target.originalJsonObject(); - static const QStringList keepKeys = - { EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), - QStringLiteral("prev_content"), ContentKey, - QStringLiteral("origin_server_ts") }; + static const QStringList keepKeys { + EventIdKey, TypeKey, QStringLiteral("room_id"), + QStringLiteral("sender"), QStringLiteral("state_key"), + QStringLiteral("prev_content"), ContentKey, + QStringLiteral("hashes"), QStringLiteral("signatures"), + QStringLiteral("depth"), QStringLiteral("prev_events"), + QStringLiteral("prev_state"), QStringLiteral("auth_events"), + QStringLiteral("origin"), QStringLiteral("origin_server_ts"), + QStringLiteral("membership") + }; std::vector> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } - , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } + , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomPowerLevels::typeId(), // { QStringLiteral("ban"), QStringLiteral("events"), // QStringLiteral("events_default"), QStringLiteral("kick"), // QStringLiteral("redact"), QStringLiteral("state_default"), // QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } } + , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } +// , { RoomHistoryVisibility::typeId(), +// { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { -- cgit v1.2.3 From 93876f06b6a1929dc757595ba4410b742402b7ab Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 25 Feb 2019 11:27:24 +0900 Subject: Room::postHtmlMessage: default message type to m.text postHtmlText becomes just a synonym for 2-arg postHtmlMessage (hopefully at least this doesn't confuse QML that is generally terrible at resolving overloads). --- lib/room.cpp | 4 ++-- lib/room.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 7e7d8505..c7723832 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1525,7 +1525,7 @@ QString Room::postPlainText(const QString& plainText) } QString Room::postHtmlMessage(const QString& plainText, const QString& html, - MessageEventType type) + MessageEventType type) { return d->sendEvent(plainText, type, new EventContent::TextContent(html, QStringLiteral("text/html"))); @@ -1533,7 +1533,7 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html, QString Room::postHtmlText(const QString& plainText, const QString& html) { - return postHtmlMessage(plainText, html, MessageEventType::Text); + return postHtmlMessage(plainText, html); } QString Room::postFile(const QString& plainText, const QUrl& localPath, diff --git a/lib/room.h b/lib/room.h index 808c6074..a9341bd2 100644 --- a/lib/room.h +++ b/lib/room.h @@ -385,7 +385,8 @@ namespace QMatrixClient QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); QString postHtmlMessage(const QString& plainText, - const QString& html, MessageEventType type); + const QString& html, + MessageEventType type = MessageEventType::Text); QString postHtmlText(const QString& plainText, const QString& html); QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); -- cgit v1.2.3 From 2213080a13a4eb472c8ac2267efcebc6f0936eb1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 25 Feb 2019 13:32:27 +0900 Subject: RoomMessageEvent: support m.in_reply_to (not spec-compliant yet); optimise away TextContent when not needed 1. The spec says "if you support rich replies you MUST support fallbacks" - this commit only adds dealing with event JSON but not with textual fallbacks. 2. TextContent is only created if there's something on top of plain body (an HTML body or a reply). --- lib/events/roommessageevent.cpp | 64 ++++++++++++++++++++++++++++++----------- lib/events/roommessageevent.h | 15 +++++++++- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index c3007fa0..8f4e0ebc 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -30,12 +30,29 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; +static const auto RelatesToKey = "m.relates_to"_ls; +static const auto MsgTypeKey = "msgtype"_ls; +static const auto BodyKey = "body"_ls; +static const auto FormattedBodyKey = "formatted_body"_ls; + +static const auto TextTypeKey = "m.text"; +static const auto NoticeTypeKey = "m.notice"; + +static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); + template TypedBase* make(const QJsonObject& json) { return new ContentT(json); } +template <> +TypedBase* make(const QJsonObject& json) +{ + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + ? new TextContent(json) : nullptr; +} + struct MsgTypeDesc { QString matrixType; @@ -44,9 +61,9 @@ struct MsgTypeDesc }; const std::vector msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make } + { { TextTypeKey, MsgType::Text, make } , { QStringLiteral("m.emote"), MsgType::Emote, make } - , { QStringLiteral("m.notice"), MsgType::Notice, make } + , { NoticeTypeKey, MsgType::Notice, make } , { QStringLiteral("m.image"), MsgType::Image, make } , { QStringLiteral("m.file"), MsgType::File, make } , { QStringLiteral("m.location"), MsgType::Location, make } @@ -78,14 +95,18 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && + json.contains(RelatesToKey)) + { + json.remove(RelatesToKey); + qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); return json; } -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; - RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), @@ -141,13 +162,17 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) { auto msgtype = content[MsgTypeKey].toString(); + bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) + { _content.reset(mt.maker(content)); + msgTypeFound = true; + } - if (!_content) + if (!msgTypeFound) { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } @@ -179,14 +204,13 @@ QMimeType RoomMessageEvent::mimeType() const static const auto PlainTextMimeType = QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; - ; } bool RoomMessageEvent::hasTextContent() const { - return content() && + return !content() || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes + msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -218,10 +242,12 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); } -TextContent::TextContent(const QString& text, const QString& contentType) +TextContent::TextContent(const QString& text, const QString& contentType, + Omittable relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) + , relatesTo(std::move(relatesTo)) { - if (contentType == "org.matrix.custom.html") + if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } @@ -233,16 +259,20 @@ TextContent::TextContent(const QJsonObject& json) // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == "org.matrix.custom.html") + if (json["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json["formatted_body"_ls].toString(); + body = json[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; body = json[BodyKey].toString(); } + const auto replyJson = json[RelatesToKey].toObject() + .value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + relatesTo = replyTo(fromJson(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const @@ -250,10 +280,12 @@ void TextContent::fillJson(QJsonObject* json) const Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), - QStringLiteral("org.matrix.custom.html")); + json->insert(QStringLiteral("format"), HtmlContentTypeId); json->insert(QStringLiteral("formatted_body"), body); } + if (!relatesTo.omitted()) + json->insert(QStringLiteral("m.relates_to"), + QJsonObject { { relatesTo->type, relatesTo->eventId } }); } LocationContent::LocationContent(const QString& geoUri, diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index d5b570f5..c2e075eb 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -92,6 +92,17 @@ namespace QMatrixClient { // Additional event content types + struct RelatesTo + { + static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + QString type; // The only supported relation so far + QString eventId; + }; + inline RelatesTo replyTo(QString eventId) + { + return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + } + /** * Rich text content for m.text, m.emote, m.notice * @@ -101,13 +112,15 @@ namespace QMatrixClient class TextContent: public TypedBase { public: - TextContent(const QString& text, const QString& contentType); + TextContent(const QString& text, const QString& contentType, + Omittable relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; + Omittable relatesTo; protected: void fillJson(QJsonObject* json) const override; -- cgit v1.2.3 From 5b5eb135be40449a6a63eb9872787bec1ecd0fc2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 25 Feb 2019 14:46:00 +0900 Subject: Have a build-wide macro for compilers that don't handle init-lists right WORKAROUND_EXTENDED_INITIALIZER_LIST -> BROKEN_INITIALIZER_LISTS is available from util.h now. --- lib/room.cpp | 10 +--------- lib/util.h | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index c7723832..9340bd58 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -72,14 +72,6 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 and older GCC's that don't handle initializer -// lists right (MSVC 2015, notably, fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'") -#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ - (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) -# define WORKAROUND_EXTENDED_INITIALIZER_LIST -#endif - class Room::Private { public: @@ -1065,7 +1057,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const total = INT_MAX; } -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST +#ifdef BROKEN_INITIALIZER_LISTS FileTransferInfo fti; fti.status = infoIt->status; fti.progress = int(progress); diff --git a/lib/util.h b/lib/util.h index 596872e2..f7f646da 100644 --- a/lib/util.h +++ b/lib/util.h @@ -52,6 +52,14 @@ template static void qAsConst(const T &&) Q_DECL_EQ_DELETE; #endif +// MSVC 2015 and older GCC's don't handle initialisation from initializer lists +// right in the absense of a constructor; MSVC 2015, notably, fails with +// "error C2440: 'return': cannot convert from 'initializer list' to ''" +#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ + (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) +# define BROKEN_INITIALIZER_LISTS +#endif + namespace QMatrixClient { // The below enables pretty-printing of enums in logs -- cgit v1.2.3 From 395ed0ca307a4cef696048b30718f8d5c99492a0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 07:55:25 +0900 Subject: Room::addNewMessageEvents: fix possible use of an invalid iterator Closes #286. --- lib/room.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 9340bd58..b0be288b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1361,8 +1361,6 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); - // FIXME: This sometimes causes a bad read: - // https://travis-ci.org/QMatrixClient/libqmatrixclient/jobs/492156899#L2596 unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); return pEvent; @@ -2057,15 +2055,21 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) it = nextPending + 1; auto* nextPendingEvt = nextPending->get(); - emit q->pendingEventAboutToMerge(nextPendingEvt, - int(nextPendingPair.second - unsyncedEvents.begin())); + const auto pendingEvtIdx = + int(nextPendingPair.second - unsyncedEvents.begin()); + emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); qDebug(EVENTS) << "Merging pending event from transaction" << nextPendingEvt->transactionId() << "into" << nextPendingEvt->id(); auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); if (transfer.status != FileTransferInfo::None) fileTransfers.insert(nextPendingEvt->id(), transfer); - unsyncedEvents.erase(nextPendingPair.second); + // After emitting pendingEventAboutToMerge() above we cannot rely + // on the previously obtained nextPendingPair.second staying valid + // because a signal handler may send another message, thereby altering + // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at + // its back so we can rely on the index staying valid at least. + unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) { totalInserted += insertedSize; -- cgit v1.2.3 From c9b3cce218c5724d511b6e0dffb8d389ce19c08f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 12:01:51 +0900 Subject: Room: avoid dangling pointers, even if not dereferenced Closes #288; fixes one more case similar to #286. Also: disconnect file transfer signals correctly in Room::postFile. --- lib/room.cpp | 111 +++++++++++++++++++++++++---------------------------------- 1 file changed, 47 insertions(+), 64 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index b0be288b..8395baca 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -263,9 +263,7 @@ class Room::Private RoomEvent* addAsPending(RoomEventPtr&& event); QString doSendEvent(const RoomEvent* pEvent); - PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); - void onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call = nullptr); + void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); template SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, @@ -1377,14 +1375,14 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) QString Room::Private::doSendEvent(const RoomEvent* pEvent) { - auto txnId = pEvent->transactionId(); + const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi(BackgroundRequest, id, pEvent->matrixType(), txnId, pEvent->contentJson())) { Room::connect(call, &BaseJob::started, q, - [this,pEvent,txnId] { - auto it = findAsPending(pEvent); + [this,txnId] { + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qWarning(EVENTS) << "Pending event for transaction" << txnId @@ -1395,14 +1393,11 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) emit q->pendingEventChanged(it - unsyncedEvents.begin()); }); Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, - this, pEvent, txnId, call)); + std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); Room::connect(call, &BaseJob::success, q, - [this,call,pEvent,txnId] { + [this,call,txnId] { emit q->messageSent(txnId, call->eventId()); - // 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); + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qDebug(EVENTS) << "Pending event for transaction" << txnId @@ -1414,23 +1409,13 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) emit q->pendingEventChanged(it - unsyncedEvents.begin()); }); } else - onEventSendingFailure(pEvent, txnId); + onEventSendingFailure(txnId); return txnId; } -Room::PendingEvents::iterator Room::Private::findAsPending( - const RoomEvent* rawEvtPtr) +void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) { - const auto comp = - [rawEvtPtr] (const auto& pe) { return pe.event() == rawEvtPtr; }; - - 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); + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qCritical(EVENTS) << "Pending event for transaction" << txnId @@ -1533,50 +1518,48 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, Q_ASSERT(localFile.isFile()); // Remote URL will only be known after upload; fill in the local path // to enable the preview while the event is pending. - auto* pEvent = d->addAsPending( - makeEvent(plainText, localFile, asGenericFile)); - const auto txnId = pEvent->transactionId(); + const auto txnId = d->addAsPending(makeEvent( + plainText, localFile, asGenericFile) + )->transactionId(); uploadFile(txnId, localPath); - QMetaObject::Connection cCompleted, cCancelled; - cCompleted = connect(this, &Room::fileTransferCompleted, this, - [cCompleted,cCancelled,this,pEvent,txnId] - (const QString& id, QUrl, const QUrl& mxcUri) { - if (id == txnId) + auto* context = new QObject(this); + connect(this, &Room::fileTransferCompleted, context, + [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + if (id == txnId) + { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) { - auto it = d->findAsPending(pEvent); - if (it != d->unsyncedEvents.end()) - { - it->setFileUploaded(mxcUri); - emit pendingEventChanged( - int(it - d->unsyncedEvents.begin())); - d->doSendEvent(pEvent); - } else { - // Normally in this situation we should instruct - // the media server to delete the file; alas, there's no - // API specced for that. - qCWarning(MAIN) << "File uploaded to" << mxcUri - << "but the event referring to it was cancelled"; - } - disconnect(cCompleted); - disconnect(cCancelled); + it->setFileUploaded(mxcUri); + emit pendingEventChanged( + int(it - d->unsyncedEvents.begin())); + d->doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was cancelled"; } - }); - cCancelled = connect(this, &Room::fileTransferCancelled, this, - [cCompleted,cCancelled,this,pEvent,txnId] (const QString& id) { - if (id == txnId) + context->deleteLater(); + } + }); + connect(this, &Room::fileTransferCancelled, this, + [context,this,txnId] (const QString& id) { + if (id == txnId) + { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) { - auto it = d->findAsPending(pEvent); - if (it != d->unsyncedEvents.end()) - { - emit pendingEventAboutToDiscard( - int(it - d->unsyncedEvents.begin())); - d->unsyncedEvents.erase(it); - emit pendingEventDiscarded(); - } - disconnect(cCompleted); - disconnect(cCancelled); + const auto idx = int(it - d->unsyncedEvents.begin()); + emit pendingEventAboutToDiscard(idx); + // See #286 on why iterator may not be valid here. + d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); + emit pendingEventDiscarded(); } - }); + context->deleteLater(); + } + }); return txnId; } -- cgit v1.2.3 From c411bd00d9a4574ee858003493b523811c50d024 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 12:49:04 +0900 Subject: prettyPrint(): only linkify http(s), ftp, mailto, magnet links Closes #278. --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index e22a1df5..fc90ea76 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -39,7 +39,7 @@ static void linkifyUrls(QString& htmlEscapedText) // Note: outer parentheses are a part of C++ raw string delimiters, not of // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" + R"(((www\.(?!\.)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" ), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] -- cgit v1.2.3 From 17c7afaa4339e7e2259718f19a80ffbf960b1a8d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 13:12:01 +0900 Subject: Linkify Matrix identifiers This is a crude interim implementation until we get new fancy Matrix URIs. --- lib/util.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/util.cpp b/lib/util.cpp index fc90ea76..d042aa34 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -38,6 +38,7 @@ static void linkifyUrls(QString& htmlEscapedText) // comma or dot // Note: outer parentheses are a part of C++ raw string delimiters, not of // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). + // Note2: yet another pair of outer parentheses are \1 in the replacement. static const QRegularExpression FullUrlRegExp(QStringLiteral( R"(((www\.(?!\.)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" ), RegExpOptions); @@ -46,6 +47,11 @@ static void linkifyUrls(QString& htmlEscapedText) static const QRegularExpression EmailAddressRegExp(QStringLiteral( R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" ), RegExpOptions); + // An interim liberal implementation of + // https://matrix.org/docs/spec/appendices.html#identifier-grammar + static const QRegularExpression MxIdRegExp(QStringLiteral( + R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:[-.a-z0-9]+))" + ), RegExpOptions); // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,& @@ -53,6 +59,8 @@ static void linkifyUrls(QString& htmlEscapedText) QStringLiteral(R"(\1\2)")); htmlEscapedText.replace(FullUrlRegExp, QStringLiteral(R"(\1)")); + htmlEscapedText.replace(MxIdRegExp, + QStringLiteral(R"(\1\2)")); } QString QMatrixClient::prettyPrint(const QString& plainText) -- cgit v1.2.3 From a78ae0e75225629563ce253308e9b88383b0ea4d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 14:11:37 +0900 Subject: Room::avatarObject Closes #268. --- lib/room.cpp | 5 +++++ lib/room.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 8395baca..5da9373e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -404,6 +404,11 @@ QUrl Room::avatarUrl() const return d->avatar.url(); } +const Avatar& Room::avatarObject() const +{ + return d->avatar; +} + QImage Room::avatar(int dimension) { return avatar(dimension, dimension); diff --git a/lib/room.h b/lib/room.h index a9341bd2..f4ecef42 100644 --- a/lib/room.h +++ b/lib/room.h @@ -33,6 +33,7 @@ namespace QMatrixClient { class Event; + class Avatar; class SyncRoomData; class RoomMemberEvent; class Connection; @@ -158,6 +159,7 @@ namespace QMatrixClient QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; + const Avatar& avatarObject() const; Q_INVOKABLE JoinState joinState() const; Q_INVOKABLE QList usersTyping() const; QList membersLeft() const; -- cgit v1.2.3 From e7a549d3d7341f66cb84761da9783f39276da116 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 20:31:19 +0900 Subject: README.md: update versioning convention for pre-releases --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0332bef..f885937c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ So far the library is typically used as a git submodule of another project (such The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), you're advised to make a recursive check out of that project (in this case, Quaternion) and update the library submodule to its master branch. -Tags starting with `v` represent released versions; `rc` mark release candidates. +Tags consisting of digits and periods represent released versions; tags ending with `~betaN` or `~rcN` mark pre-releases. ### Pre-requisites - a Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) -- cgit v1.2.3 From e0a0542a812937b189f2adc7da9f2c9bba2d89b1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Feb 2019 20:56:05 +0900 Subject: README.md: use dash instead of tilde in pre-releases [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f885937c..ab275a35 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ So far the library is typically used as a git submodule of another project (such The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), you're advised to make a recursive check out of that project (in this case, Quaternion) and update the library submodule to its master branch. -Tags consisting of digits and periods represent released versions; tags ending with `~betaN` or `~rcN` mark pre-releases. +Tags consisting of digits and periods represent released versions; tags ending with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, it is advised to replace a dash with a tilde. ### Pre-requisites - a Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) -- cgit v1.2.3 From 39d44789d0f378b29d2c15994e8fa630edcdb408 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 27 Feb 2019 11:21:08 +0900 Subject: BaseJob::abandon() fixes 1. It should work with non-started jobs now (Closes #289). 2. It should allow clients to still handle `finished()` instead of cutting them off right before emitting the signal. --- lib/jobs/basejob.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 628d10ec..8c3381ae 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -603,7 +603,7 @@ void BaseJob::setStatus(Status s) if (d->status == s) return; - if (!d->connection->accessToken().isEmpty()) + if (d->connection && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; @@ -618,9 +618,8 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { - beforeAbandon(d->reply.data()); + beforeAbandon(d->reply ? d->reply.data() : nullptr); setStatus(Abandoned); - this->disconnect(); if (d->reply) d->reply->disconnect(this); emit finished(this); -- cgit v1.2.3 From 46cb751f73ca4234d5600e0c76e7f93c74278ef5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 27 Feb 2019 15:28:39 +0900 Subject: Connection::stopSync: undo the sync loop --- lib/connection.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 998282d3..26b40c03 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -466,7 +466,10 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { void Connection::stopSync() { - if (d->syncJob) + // If there's a sync loop, break it + disconnect(this, &Connection::syncDone, + this, &Connection::syncLoopIteration); + if (d->syncJob) // If there's an ongoing sync job, stop it too { d->syncJob->abandon(); d->syncJob = nullptr; -- cgit v1.2.3 From 0975f96207300b31279c63eaea597d2e9c435532 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 27 Feb 2019 15:28:57 +0900 Subject: qmc-example: use Connection::syncLoop --- examples/qmc-example.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 421ead27..9d6f2f39 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -147,14 +147,12 @@ void QMCTest::onNewRoom(Room* r) void QMCTest::run() { c->setLazyLoading(true); - c->sync(); + c->syncLoop(); connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests); connect(c.data(), &Connection::syncDone, c.data(), [this] { cout << "Sync complete, " << running.size() << " test(s) in the air: " << running.join(", ").toStdString() << endl; - if (!running.isEmpty()) - c->sync(10000); - else + if (running.isEmpty()) conclude(); }); } @@ -491,6 +489,7 @@ void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) void QMCTest::conclude() { + c->stopSync(); auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; if (!failed.isEmpty() || !running.isEmpty()) succeededRec += " of " % -- cgit v1.2.3 From b467b0816f5f6816778f90b55a9d0b5437310fd5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 27 Feb 2019 20:49:59 +0900 Subject: Refresh CONTRIBUTING.md --- CONTRIBUTING.md | 141 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 32 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ee39eec..7b534c32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ For general discussion, feel free to use our Matrix room: [#quaternion:matrix.org](https://matrix.to/#/#quaternion:matrix.org). If you're new to the project (or FLOSS in general), -[issues tagged as simple](https://github.com/QMatrixClient/libQMatrixClient/labels/simple) +[issues tagged as easy](https://github.com/QMatrixClient/libQMatrixClient/labels/easy) are smaller tasks that may typically take 1-3 days. You are welcome aboard! @@ -108,11 +108,12 @@ use either of the following contacts: In any of these two options, _indicate that you have such information_ (do not share the information yet), and we'll tell you the next steps. -By default, we will give credit to anyone who reports a vulnerability so that -we can fix it. If you want to remain anonymous or pseudonymous instead, please -let us know that; we will gladly respect your wishes. If you provide a fix as -a PR, you have no way to remain anonymous but we still accept contributors with -pseudonyms. +By default, we will give credit to anyone who reports a vulnerability in +a responsible way so that we can fix it before public disclosure. If you want +to remain anonymous or pseudonymous instead, please let us know that; we will +gladly respect your wishes. If you provide a fix as a PR, you have no way +to remain anonymous (and you also disclose the vulnerability thereby) so this +is not the right way, unless the vulnerability is already made public. ## Documentation changes @@ -154,22 +155,40 @@ If you don't plan/have substantial contributions, you can end reading here. Furt The code should strive to be DRY (don't repeat yourself), clear, and obviously correct. Some technical debt is inevitable, just don't bankrupt us with it. Refactoring is welcome. ### Generated C++ code for CS API -The code in lib/csapi, although it resides in Git, is actually generated from the official Matrix Swagger/OpenAPI definition files. If you're unhappy with something in that directory and want to improve the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling the below text can be found in .travis.yml (our Travis CI configuration actually regenerates those files upon every build). The generating sequence only works with CMake atm; patches to enable it with qmake are (you guessed it) very welcome. +The code in lib/csapi, lib/identity and lib/application-service, although +it resides in Git, is actually generated from the official Matrix +Swagger/OpenAPI definition files. If you're unhappy with something in these +directories and want to improve the code, you have to understand the way these +files are produced and setup some additional tooling. The shortest possible +procedure resembling the below text can be found in .travis.yml (our Travis CI +configuration actually regenerates those files upon every build). +The generating sequence only works with CMake atm; +patches to enable it with qmake are (you guessed it) very welcome. #### Why generate the code at all? -Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). +Because before both original authors of libQMatrixClient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep, coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). #### Prerequisites for CS API code generation 1. Get the source code of GTAD and its dependencies, e.g. using the command: `git clone --recursive https://github.com/KitsuneRal/gtad.git` 2. Build GTAD: in the source code directory, do `cmake . && cmake --build .` (you might need to pass `-DCMAKE_PREFIX_PATH=`, similar to libQMatrixClient itself). 3. Get the Matrix CS API definitions that are included in the matrix-doc repo: `git clone https://github.com/QMatrixClient/matrix-doc.git` (QMatrixClient/matrix-doc is a fork that's known to produce working code; you may want to use your own fork if you wish to alter something in the API). -#### Generating lib/csapi contents -1. Pass additional configuration to CMake when configuring libQMatrixClient, namely: `-DMATRIX_DOC_PATH= -DGTAD_PATH=`. If everything's right, these two CMake variables will be mentioned in CMake output and will trigger configuration of an additional build target, see the next step. -2. Generate the code: `cmake --build --target update-api`; if you use CMake with GNU Make, you can just do `make update-api` instead. Building this target will create (overwriting without warning) .h and .cpp files in lib/csapi directory for all YAML files it can find in `matrix-doc/api/client-server`. -3. Once you've done that, you can build the library as usual; rerunning CMake is recommended if the list of generated files has changed. - -#### Changing things in lib/csapi +#### Generating CS API contents +1. Pass additional configuration to CMake when configuring libQMatrixClient: + `-DMATRIX_DOC_PATH= -DGTAD_PATH=`. + If everything's right, these two CMake variables will be mentioned in + CMake output and will trigger configuration of an additional build target, + see the next step. +2. Generate the code: `cmake --build --target update-api`; + if you use CMake with GNU Make, you can just do `make update-api` instead. + Building this target will create (overwriting without warning) `.h` and `.cpp` + files in `lib/csapi`, `lib/identity`, `lib/application-service` for all + YAML files it can find in `matrix-doc/api/client-server` and other files + in `matrix-doc/api` these depend on. +3. Once you've done that, you can build the library as usual; rerunning CMake + is recommended if the list of generated files has changed. + +#### Changing generated code See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQMatrixClient are described here. GTAD uses the following three kinds of sources: @@ -179,18 +198,18 @@ GTAD uses the following three kinds of sources: The mustache files have a templated (not in C++ sense) definition of a network job, deriving from BaseJob; each job class is prepended, if necessary, with data structure definitions used by this job. The look of those files is hideous for a newcomer; the fact that there's no highlighter for the combination of Mustache (originally a web templating language) and C++ doesn't help things, either. To slightly simplify things some more or less generic constructs are defined in gtad.yaml (see its "mustache:" section). Adventurous souls that would like to figure what's going on in these files should speak up in the libQMatrixClient room - I (Kitsune) will be very glad to help you out. -The types map in gtad.yaml is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": +The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": * `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible. * `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around. * `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h` - a substitute for `std::optional` from C++17 (we're still at C++14 yet). * `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). * `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. -Instead of relying on the event structure definition in the OpenAPI files, gtad.yaml uses pointers to libQMatrixClient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. +Instead of relying on the event structure definition in the OpenAPI files, `gtad.yaml` uses pointers to libQMatrixClient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. ### Library API and doc-comments -Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes if that bothers you. Some parts are not even documented; add doc-comments to them is highly encouraged. +Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes and if that bothers you. Some parts are not even documented; adding doc-comments to them is highly encouraged. Calls, data structures and other symbols not intended for use by clients should _not_ be exposed in (public) .h files, unless they are necessary to declare other public symbols. In particular, this involves private members (functions, typedefs, or variables) in public classes; use pimpl idiom to hide implementation details as much as possible. @@ -198,18 +217,46 @@ Note: As of now, all header files of libQMatrixClient are considered public; thi ### Qt-flavoured C++ -This is our primary language. We don't have a particular code style _as of yet_ but some rules-of-thumb are below: -* 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - we'll thank you for fixing it. +This is our primary language. We don't have a particular code style _as of yet_ +but some rules-of-thumb are below: +* 4-space indents, no tabs, no trailing spaces, no last empty lines. If you + spot the code abusing these - we'll thank you for fixing it. * Prefer keeping lines within 80 characters. -* Braces after if's, while's, do's, function signatures etc. take a separate line. Keeping the opening brace on the same line is still ok. -* A historical deviation from the usual Qt code format conventions is an extra indent inside _classes_ (access specifiers go at +4 spaces to the base, members at +8 spaces) but not _structs_ (members at +4 spaces). This may change in the future for something more conventional. -* Please don't make "hypocritical structs" with protected or private members. Just make them classes instead. -* For newly created classes, keep to [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - make sure to read about the rule of zero if you haven't before, it's not what you might think it is. -* Qt containers are generally preferred to STL containers; however, there are notable exceptions, and libQMatrixClient already uses them: +* Braces after if's, while's, do's, function signatures etc. take + a separate line. Keeping the opening brace on the same line is still ok. +* A historical deviation from the usual Qt code format conventions is an extra + indent inside _classes_ (access specifiers go at +4 spaces to the base, + members at +8 spaces) but not _structs_ (members at +4 spaces). This may + change in the future for something more conventional. +* Please don't make "hypocritical structs" with protected or private members. + In general, `struct` is used to denote a plain-old-data structure, rather + than data+behaviour. If you need access control or are adding yet another + non-trivial (construction, assignment) member function to a `struct`, + just make it a `class` instead. +* For newly created classes, keep to + [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - + make sure to read about the rule of zero if you haven't before, it's not + what you might think it is. +* Qt containers are generally preferred to STL containers; however, there are + notable exceptions, and libQMatrixClient already uses them: * `std::array` and `std::deque` have no direct counterparts in Qt. - * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. Examples of that are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but see the next point and also consider if you can supply a reasonable copy/default constructor. - * STL containers can be freely used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up code where you don't need COW, or when dealing with uncopyable data structures (see the previous point). However, exposing STL containers in the API is not encouraged (except where absolutely necessary, e.g. we use `std::deque` for a timeline). Exposing STL containers or iterators in API intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work and therefore unlikely to be accepted into `master`. -* Use `QVector` instead of `QList` where possible - see a [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. + * Because of COW semantics, Qt containers cannot hold uncopyable classes. + Classes without a default constructor are a problem too. Examples of that + are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but + see the next point and also consider if you can supply a reasonable + copy/default constructor. + * STL containers can be freely used in code internal to a translation unit + (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. + It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up + code where you don't need COW, or when dealing with uncopyable + data structures (see the previous point). However, exposing STL containers + in the API is not encouraged (except where absolutely necessary, e.g. we use + `std::deque` for a timeline). Exposing STL containers or iterators in API + intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work + and therefore unlikely to be accepted into `master`. +* Use `QVector` instead of `QList` where possible - see the + [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) + for details. ### Automated tests @@ -253,7 +300,13 @@ your commit into it (with an explanation what it is about and why). ### Standard checks -The following warnings configuration is applied with GCC and Clang when using CMake: `-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` (the last one is to mute a warning triggered by Qt code for debug logging). We don't turn most of the warnings to errors but please treat them as such. In Qt Creator, the following line can be used with the Clang code model (before Qt Creator 4.7 you should explicitly enable the Clang code model plugin): `-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables` +The following warnings configuration is applied with GCC and Clang when using CMake: +`-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` +(the last one is to mute a warning triggered by Qt code for debug logging). +We don't turn most of the warnings to errors but please treat them as such. +In Qt Creator, the following line can be used with the Clang code model +(before Qt Creator 4.7 you should explicitly enable the Clang code model plugin): +`-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables -Wno-unknown-attributes -Wno-comma`. ### Continuous Integration @@ -261,7 +314,16 @@ We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) a ### Other tools -If you know how to use clang-tidy, here's a list of checks we do and do not use (a leading hyphen means a disabled check, an asterisk is a wildcard): `*,cert-env33-c,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-cppcoreguidelines-pro-type-const-cast,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-special-member-functions,-google-build-using-namespace,-google-readability-braces-around-statements,-hicpp-*,-llvm-*,-misc-unused-parameters,-misc-noexcept-moveconstructor,-modernize-use-using,-readability-braces-around-statements,readability-identifier-naming,-readability-implicit-bool-cast,-clang-diagnostic-*,-clang-analyzer-*`. If you're on CLion, you can simply copy-paste the above list into the Clang-Tidy inspection configuration. In Qt Creator 4.6 and newer, one can enable clang-tidy and clazy (clazy level 1 eats away CPU but produces some very relevant and unobvious notices, such as possible unintended copying of a Qt container, or unguarded null pointers). +Recent versions of Qt Creator and CLion can automatically run your code through +clang-tidy. The following list of clang-tidy checks slows Qt Creator analysis +quite considerably but gives a good insight without too many false positives: +`-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-move-forwarding-reference,bugprone-string-constructor,bugprone-undefined-memory-manipulation,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl03-c,cert-dcl21-cpp,cert-dcl50-cpp,cert-dcl54-cpp,cert-dcl58-cpp,cert-env33-c,cert-err09-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-err61-cpp,cert-fio38-c,cert-flp30-c,cert-msc30-c,cert-msc50-cpp,cert-oop11-cpp,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-slicing,hicpp-deprecated-headers,hicpp-invalid-access-moved,hicpp-member-init,hicpp-move-const-arg,hicpp-named-parameter,hicpp-new-delete-operators,hicpp-static-assert,hicpp-undelegated-constructor,hicpp-use-*,misc-misplaced-const,misc-new-delete-overloads,misc-non-copyable-objects,misc-redundant-expression,misc-static-assert,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,misc-unused-*,modernize-loop-convert,modernize-pass-by-value,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-*,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-*,performance-move-*,performance-type-promotion-in-math-fn,performance-unnecessary-*,readability-delete-null-pointer,readability-else-after-return,readability-inconsistent-declaration-parameter-name,readability-misleading-indentation,readability-redundant-*,readability-simplify-boolean-expr,readability-static-definition-in-anonymous-namespace,readability-uniqueptr-delete-release`. + +Qt Creator, in addition, knows about clazy, an even deeper Qt-aware static +analysis tool. Even level 1 clazy eats away CPU but produces some very relevant +and unobvious notices, such as possible unintended copying of a Qt container, +or unguarded null pointers. You can use this time to time (see Analyze menu in +Qt Creator) instead of loading your machine with deep runtime analysis. ## Git commit messages @@ -284,9 +346,24 @@ C++ is unfortunately not very coherent about SDK/package management, and we try Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for automated testing, so PRs onboarding a test framework will be considered with much gratitude. Some cases need additional explanation: -* Before rolling out your own super-optimised container or algorithm written from scratch, take a good long look through documentation on Qt and C++ standard library. Please try to reuse the existing facilities as much as possible. -* You should have a good reason (or better several ones) to add a component from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates but so far none of these components survived as libQMatrixClient deps. So we are cautious. -* Never forget that libQMatrixClient is aimed to be a non-visual library; QtGui in dependencies is only driven by (entirely offscreen) dealing with QPixmaps. While there's a bunch of visual code (in C++ and QML) shared between libQMatrixClient-enabled _applications_, this is likely to end up in a separate (libQMatrixClient-enabled) library, rather than libQMatrixClient itself. +* Before rolling out your own super-optimised container or algorithm written + from scratch, take a good long look through documentation on Qt and + C++ standard library. Please try to reuse the existing facilities + as much as possible. +* You should have a good reason (or better several ones) to add a component + from KDE Frameworks. We don't rule this out and there's no prejudice against + KDE; it just so happened that KDE Frameworks is one of most obvious + reuse candidates but so far none of these components survived + as libQMatrixClient deps. So we are cautious. Extra notice to KDE folks: + I'll be happy if an addon library on top of libQMatrixClient is made using + KDE facilities, and I'm willing to take part in its evolution; but please + also respect LXDE people who normally don't have KDE frameworks installed. +* Never forget that libQMatrixClient is aimed to be a non-visual library; + QtGui in dependencies is only driven by (entirely offscreen) dealing with + QImages. While there's a bunch of visual code (in C++ and QML) shared + between libQMatrixClient-enabled _applications_, this is likely to end up + in a separate (libQMatrixClient-enabled) library, rather than + libQMatrixClient itself. ## Attribution -- cgit v1.2.3 From 8c685b4ae5b47e55a55f23e16ccbda0132cb60c5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 10 Mar 2019 16:59:55 +0900 Subject: Room::checkVersion(): be tolerant to already upgraded rooms --- lib/connection.cpp | 3 +-- lib/room.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 26b40c03..59aca025 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -271,8 +271,7 @@ void Connection::reloadCapabilities() Q_ASSERT(!d->capabilities.roomVersions.omitted()); emit capabilitiesLoaded(); for (auto* r: d->roomMap) - if (r->joinState() == JoinState::Join && r->successorId().isEmpty()) - r->checkVersion(); + r->checkVersion(); }); } diff --git a/lib/room.cpp b/lib/room.cpp index 5da9373e..f2e03e94 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1634,7 +1634,7 @@ void Room::checkVersion() { const auto defaultVersion = connection()->defaultRoomVersion(); const auto stableVersions = connection()->stableRoomVersions(); - Q_ASSERT(!defaultVersion.isEmpty() && successorId().isEmpty()); + Q_ASSERT(!defaultVersion.isEmpty()); // This method is only called after the base state has been loaded // or the server capabilities have been loaded. emit stabilityUpdated(defaultVersion, stableVersions); -- cgit v1.2.3 From f13d54bd9931a340af862cc0a03af2ac68fe5e06 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 14 Mar 2019 07:42:24 +0900 Subject: Fix read receipts and redactions on v3 rooms Previously slashes in eventIds (that come plenty in v3 due to base64 encoding) were not properly encoded - they are now. --- lib/jobs/basejob.cpp | 2 +- lib/room.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 8c3381ae..f738ce7a 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -186,7 +186,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl, if (!pathBase.endsWith('/') && !path.startsWith('/')) pathBase.push_back('/'); - baseUrl.setPath( pathBase + path ); + baseUrl.setPath(pathBase + path, QUrl::TolerantMode); baseUrl.setQuery(query); return baseUrl; } diff --git a/lib/room.cpp b/lib/room.cpp index f2e03e94..dbddad05 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -579,8 +579,8 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { if ((*upToMarker)->senderId() != q->localUser()->id()) { - connection->callApi(id, "m.read", - (*upToMarker)->id()); + connection->callApi(id, QStringLiteral("m.read"), + QUrl::toPercentEncoding((*upToMarker)->id())); break; } } @@ -1734,8 +1734,8 @@ void Room::unban(const QString& userId) void Room::redactEvent(const QString& eventId, const QString& reason) { - connection()->callApi( - id(), eventId, connection()->generateTxnId(), reason); + connection()->callApi(id(), + QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason); } void Room::uploadFile(const QString& id, const QUrl& localFilename, -- cgit v1.2.3 From 6577320f8653fbd99a100a844d7b42a46da5f45a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 17 Mar 2019 09:03:34 +0900 Subject: RoomMemberEvent: sanitize user display names MemberEventContent::displayName() will strip away Unicode text direction override characters. Direct access to JSON can still provide "raw" data. --- lib/events/roommemberevent.cpp | 2 +- lib/util.cpp | 10 +++++++++- lib/util.h | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index a5ac3c5f..6da76526 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -52,7 +52,7 @@ using namespace QMatrixClient; MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) - , displayName(json["displayname"_ls].toString()) + , displayName(sanitized(json["displayname"_ls].toString())) , avatarUrl(json["avatar_url"_ls].toString()) { } diff --git a/lib/util.cpp b/lib/util.cpp index d042aa34..2744d45f 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -63,10 +63,18 @@ static void linkifyUrls(QString& htmlEscapedText) QStringLiteral(R"(\1\2)")); } +QString QMatrixClient::sanitized(const QString& plainText) +{ + auto text = plainText; + text.remove(QChar(0x202e)); + text.remove(QChar(0x202d)); + return text.toHtmlEscaped(); +} + QString QMatrixClient::prettyPrint(const QString& plainText) { auto pt = QStringLiteral("") + - plainText.toHtmlEscaped() + QStringLiteral(""); + sanitized(plainText).toHtmlEscaped() + QStringLiteral(""); pt.replace('\n', QStringLiteral("
")); linkifyUrls(pt); diff --git a/lib/util.h b/lib/util.h index f7f646da..beb3c697 100644 --- a/lib/util.h +++ b/lib/util.h @@ -296,7 +296,12 @@ namespace QMatrixClient return std::make_pair(last, sLast); } - /** Pretty-prints plain text into HTML + /** Sanitize the text before showing in HTML + * This does toHtmlEscaped() and removes Unicode BiDi marks. + */ + QString sanitized(const QString& plainText); + + /** Pretty-print plain text into HTML * This includes HTML escaping of <,>,",& and URLs linkification. */ QString prettyPrint(const QString& plainText); -- cgit v1.2.3 From 4a7e1a8c8a1940827ba9aea0bd830ef9dbf912ed Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 17 Mar 2019 18:58:49 +0900 Subject: User: strip RLO/LRO markers on renaming as well Continuation of work on #545. --- lib/user.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/user.cpp b/lib/user.cpp index eec41957..93cfbffd 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -264,8 +264,9 @@ void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, void User::rename(const QString& newName) { - auto job = connection()->callApi(id(), newName); - connect(job, &BaseJob::success, this, [=] { updateName(newName); }); + const auto actualNewName = sanitized(newName); + connect(connection()->callApi(id(), actualNewName), + &BaseJob::success, this, [=] { updateName(actualNewName); }); } void User::rename(const QString& newName, const Room* r) @@ -279,10 +280,11 @@ void User::rename(const QString& newName, const Room* r) } Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); + const auto actualNewName = sanitized(newName); MemberEventContent evtC; - evtC.displayName = newName; - auto job = r->setMemberState(id(), RoomMemberEvent(move(evtC))); - connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); + evtC.displayName = actualNewName; + connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))), + &BaseJob::success, this, [=] { updateName(actualNewName, r); }); } bool User::setAvatar(const QString& fileName) -- cgit v1.2.3 From bc27d9cdcc5cb71aec12f2e3f15b5762a93b721f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 17 Mar 2019 19:29:29 +0900 Subject: prettyPrint: do not apply sanitized() Only display names should be sanitized; messages are only HTML-escaped. --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 2744d45f..59e373c2 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -74,7 +74,7 @@ QString QMatrixClient::sanitized(const QString& plainText) QString QMatrixClient::prettyPrint(const QString& plainText) { auto pt = QStringLiteral("") + - sanitized(plainText).toHtmlEscaped() + QStringLiteral(""); + plainText.toHtmlEscaped() + QStringLiteral(""); pt.replace('\n', QStringLiteral("
")); linkifyUrls(pt); -- cgit v1.2.3 From 9e0897ed1c1b4bbec6333bd6674bd8db737b13cb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 21 Mar 2019 15:41:06 +0900 Subject: Room::displayName: fix NOTIFY signal for Q_PROPERTY --- lib/room.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.h b/lib/room.h index f4ecef42..3da2d4e7 100644 --- a/lib/room.h +++ b/lib/room.h @@ -88,7 +88,7 @@ namespace QMatrixClient Q_PROPERTY(QString name READ name NOTIFY namesChanged) Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) - Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) -- cgit v1.2.3 From a15c26ccc514fc405fd06d9a88dc333f104fba78 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 21 Mar 2019 15:49:10 +0900 Subject: sanitized(): revert to only dropping Unicode RLO/LRO markers (no HTML escaping) Because user display names (in particular) can be used in non-HTML context. Clients should take care about HTML escaping where appropriate. --- lib/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.cpp b/lib/util.cpp index 59e373c2..6ed93ba0 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -68,7 +68,7 @@ QString QMatrixClient::sanitized(const QString& plainText) auto text = plainText; text.remove(QChar(0x202e)); text.remove(QChar(0x202d)); - return text.toHtmlEscaped(); + return text; } QString QMatrixClient::prettyPrint(const QString& plainText) -- cgit v1.2.3 From 19b94c0643b6f1f1f4fa327e50b69fb11675cf21 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 23 Mar 2019 15:51:04 +0900 Subject: Update to the latest CS API definitions No breaking changes; GetAccountDataJob/GetAccountDataPerRoomJob added. --- lib/csapi/account-data.cpp | 28 ++++++++++++++++++++ lib/csapi/account-data.h | 66 ++++++++++++++++++++++++++++++++++++++++++---- lib/csapi/capabilities.h | 8 +++--- lib/csapi/room_upgrades.h | 4 +-- 4 files changed, 94 insertions(+), 12 deletions(-) diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 5021c73a..96b32a92 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -21,6 +21,20 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, setRequestData(Data(toJson(content))); } +QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/user/" % userId % "/account_data/" % type); +} + +static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob"); + +GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) + : BaseJob(HttpVerb::Get, GetAccountDataJobName, + basePath % "/user/" % userId % "/account_data/" % type) +{ +} + static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob"); SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) @@ -30,3 +44,17 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const setRequestData(Data(toJson(content))); } +QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type); +} + +static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob"); + +GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) + : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName, + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) +{ +} + diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index f3656a14..b067618f 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -22,8 +22,8 @@ namespace QMatrixClient public: /*! Set some account_data for the user. * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. @@ -33,6 +33,33 @@ namespace QMatrixClient explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); }; + /// Get some account_data for the user. + /// + /// Get some account_data for the client. This config is only visible to the user + /// that set the account_data. + class GetAccountDataJob : public BaseJob + { + public: + /*! Get some account_data for the user. + * \param userId + * The ID of the user to get account_data for. The access token must be + * authorized to make requests for this user ID. + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataJob(const QString& userId, const QString& type); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetAccountDataJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type); + + }; + /// Set some account_data for the user. /// /// Set some account_data for the client on a given room. This config is only @@ -43,10 +70,10 @@ namespace QMatrixClient public: /*! Set some account_data for the user. * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. * \param roomId - * The id of the room to set account_data on. + * The ID of the room to set account_data on. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. @@ -55,4 +82,33 @@ namespace QMatrixClient */ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); }; + + /// Get some account_data for the user. + /// + /// Get some account_data for the client on a given room. This config is only + /// visible to the user that set the account_data. + class GetAccountDataPerRoomJob : public BaseJob + { + public: + /*! Get some account_data for the user. + * \param userId + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. + * \param roomId + * The ID of the room to get account_data for. + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetAccountDataPerRoomJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type); + + }; } // namespace QMatrixClient diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index 39e2f4d1..06a8bf0d 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -39,8 +39,8 @@ namespace QMatrixClient QHash available; }; - /// Gets information about the server's supported feature set - /// and other relevant capabilities. + /// The custom capabilities the server supports, using the + /// Java package naming convention. struct Capabilities { /// Capability to indicate if the user can change their password. @@ -68,8 +68,8 @@ namespace QMatrixClient // Result properties - /// Gets information about the server's supported feature set - /// and other relevant capabilities. + /// The custom capabilities the server supports, using the + /// Java package naming convention. const Capabilities& capabilities() const; protected: diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h index 6f712f10..4da5941a 100644 --- a/lib/csapi/room_upgrades.h +++ b/lib/csapi/room_upgrades.h @@ -13,9 +13,7 @@ namespace QMatrixClient /// Upgrades a room to a new room version. /// - /// Upgrades the given room to a particular room version, migrating as much - /// data as possible over to the new room. See the `room_upgrades <#room-upgrades>`_ - /// module for more information on what this entails. + /// Upgrades the given room to a particular room version. class UpgradeRoomJob : public BaseJob { public: -- cgit v1.2.3 From 23352250c9b9f9fa7d1d46294f8c1a7de1e19f61 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 23 Mar 2019 20:43:02 +0900 Subject: Room::downloadFile(): Tighten URL validations Check the URL before passing over to Connection::downloadFile(), not only the file name. --- lib/events/eventcontent.cpp | 6 ++++++ lib/events/eventcontent.h | 2 ++ lib/room.cpp | 9 ++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 9a5e872c..77f756cd 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -50,6 +50,12 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } +bool FileInfo::isValid() const +{ + return url.scheme() == "mxc" + && (url.authority() + url.path()).count('/') == 1; +} + void FileInfo::fillInfoJson(QJsonObject* infoJson) const { Q_ASSERT(infoJson); diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 0588c0e2..ab31a75d 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -94,6 +94,8 @@ namespace QMatrixClient FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); + bool isValid() const; + void fillInfoJson(QJsonObject* infoJson) const; /** diff --git a/lib/room.cpp b/lib/room.cpp index dbddad05..411a17d6 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1785,7 +1785,14 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) Q_ASSERT(false); return; } - const auto fileUrl = event->content()->fileInfo()->url; + const auto* const fileInfo = event->content()->fileInfo(); + if (!fileInfo->isValid()) + { + qCWarning(MAIN) << "Event" << eventId + << "has an empty or malformed mxc URL; won't download"; + return; + } + const auto fileUrl = fileInfo->url; auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { -- cgit v1.2.3 From 866b3d2155c0c7f2a58bb1a3c6355129361cb53a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 24 Mar 2019 18:51:08 +0900 Subject: linkifyUrls(): fix linkification of emails containing "www." Closes #303. --- lib/util.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/util.cpp b/lib/util.cpp index 6ed93ba0..8d16cfc8 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -38,14 +38,14 @@ static void linkifyUrls(QString& htmlEscapedText) // comma or dot // Note: outer parentheses are a part of C++ raw string delimiters, not of // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). - // Note2: yet another pair of outer parentheses are \1 in the replacement. + // Note2: the next-outer parentheses are \N in the replacement. static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(((www\.(?!\.)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" ), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] static const QRegularExpression EmailAddressRegExp(QStringLiteral( - R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" + R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))" ), RegExpOptions); // An interim liberal implementation of // https://matrix.org/docs/spec/appendices.html#identifier-grammar @@ -53,7 +53,7 @@ static void linkifyUrls(QString& htmlEscapedText) R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:[-.a-z0-9]+))" ), RegExpOptions); - // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,& + // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," htmlEscapedText.replace(EmailAddressRegExp, QStringLiteral(R"(\1\2)")); -- cgit v1.2.3 From d8147d4ad8493ae9de94aee8a222a24d000a7c96 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 26 Mar 2019 11:58:12 +0900 Subject: Room::canSwitchVersions(): return false on tombstoned rooms A softer take on #306. --- lib/room.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 411a17d6..4ce1bee3 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -600,6 +600,9 @@ void Room::markAllMessagesAsRead() bool Room::canSwitchVersions() const { + if (!successorId().isEmpty()) + return false; // Noone can upgrade a room that's already upgraded + // TODO, #276: m.room.power_levels const auto* plEvt = d->currentState.value({"m.room.power_levels", ""}); -- cgit v1.2.3 From 0e439f23ff96a219ad7156e40294f88d44d55361 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 27 Mar 2019 19:06:34 +0900 Subject: Connection::domain() --- lib/connection.cpp | 5 +++++ lib/connection.h | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 59aca025..07c24c92 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -788,6 +788,11 @@ QUrl Connection::homeserver() const return d->data->baseUrl(); } +QString Connection::domain() const +{ + return d->userId.section(':', 1); +} + Room* Connection::room(const QString& roomId, JoinStates states) const { Room* room = d->roomMap.value({roomId, false}, nullptr); diff --git a/lib/connection.h b/lib/connection.h index b22d63da..ea5be51a 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -104,6 +104,7 @@ namespace QMatrixClient Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) @@ -241,7 +242,10 @@ namespace QMatrixClient /** Get the full list of users known to this account */ QMap users() const; + /** Get the base URL of the homeserver to connect to */ QUrl homeserver() const; + /** Get the domain name used for ids/aliases on the server */ + QString domain() const; /** Find a room by its id and a mask of applicable states */ Q_INVOKABLE Room* room(const QString& roomId, JoinStates states = JoinState::Invite|JoinState::Join) const; -- cgit v1.2.3 From c659d18f36aa9f587003d5f50a9734c85d684a7c Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 27 Mar 2019 19:09:28 +0900 Subject: qmc-example: add a couple homeserver data sanity checks --- examples/qmc-example.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 9d6f2f39..bd9190b9 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -101,6 +101,8 @@ QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) void QMCTest::setupAndRun() { + Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); + Q_ASSERT(c->domain() == c->userId().section(':', 1)); cout << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; cout << "Access token: " << c->accessToken().toStdString() << endl; -- cgit v1.2.3 From 3d13e32530990569a418449c86d9848fd71490e4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 29 Mar 2019 13:02:12 +0900 Subject: Room::processRedaction(): avoid accidental creation of entries in currentState; cleanup --- lib/room.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 4ce1bee3..38f5c4ac 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1959,10 +1959,10 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) { const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; Q_ASSERT(currentState.contains(evtKey)); - if (currentState[evtKey] == oldEvent.get()) + if (currentState.value(evtKey) == oldEvent.get()) { Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState - qCDebug(MAIN).nospace() << "Reverting state " + qCDebug(MAIN).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); // Retarget the current state to the newly made event. if (q->processStateEvent(*ti)) @@ -2163,7 +2163,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); - if (!is(e)) + if (!is(e)) // Room member events are too numerous qCDebug(EVENTS) << "Room state event:" << e; return visit(e -- cgit v1.2.3 From 460aa0350c22312829d3e2a3d8556221b3f89173 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 29 Mar 2019 13:26:36 +0900 Subject: Room::processStateEvent, User: take the previous membership state from oldStateEvent memberJoinState() just happens to return the not-yet-updated state, making its use around state changes very sensitive to moving things around. The event's own prevContent is unsigned, therefore untrusted. --- lib/room.cpp | 24 ++++++++++++++++-------- lib/user.cpp | 13 ++++++------- lib/user.h | 6 +++++- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 38f5c4ac..0305cf7b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1177,7 +1177,11 @@ void Room::Private::insertMemberIntoMap(User *u) const auto userName = u->name(q); // If there is exactly one namesake of the added user, signal member renaming // for that other one because the two should be disambiguated now. - auto namesakes = membersMap.values(userName); + const auto namesakes = membersMap.values(userName); + + // Callers should check they are not adding an existing user once more. + Q_ASSERT(!namesakes.contains(u)); + if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -2189,16 +2193,20 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit avatarChanged(); return AvatarChange; } - , [this] (const RoomMemberEvent& evt) { + , [this,oldStateEvent] (const RoomMemberEvent& evt) { auto* u = user(evt.userId()); - u->processEvent(evt, this); - if (u == localUser() && memberJoinState(u) == JoinState::Invite + const auto* oldMemberEvent = + static_cast(oldStateEvent); + u->processEvent(evt, this, oldMemberEvent == nullptr); + const auto prevMembership = oldMemberEvent + ? oldMemberEvent->membership() : MembershipType::Leave; + if (u == localUser() && evt.membership() == MembershipType::Invite && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); - if( evt.membership() == MembershipType::Join ) + if (evt.membership() == MembershipType::Join) { - if (memberJoinState(u) != JoinState::Join) + if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); connect(u, &User::nameAboutToChange, this, @@ -2214,9 +2222,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit userAdded(u); } } - else if( evt.membership() != MembershipType::Join ) + else if (evt.membership() != MembershipType::Join) { - if (memberJoinState(u) == JoinState::Join) + if (prevMembership == MembershipType::Join) { if (evt.membership() == MembershipType::Invite) qCWarning(MAIN) << "Invalid membership change:" << evt; diff --git a/lib/user.cpp b/lib/user.cpp index 93cfbffd..c373a067 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -379,19 +379,18 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room) +void User::processEvent(const RoomMemberEvent& event, const Room* room, + bool firstMention) { Q_ASSERT(room); + + if (firstMention) + ++d->totalRooms; + if (event.membership() != MembershipType::Invite && event.membership() != MembershipType::Join) return; - auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave && - (event.membership() == MembershipType::Join || - event.membership() == MembershipType::Invite); - if (aboutToEnter) - ++d->totalRooms; - auto newName = event.displayName(); // `bridged` value uses the same notification signal as the name; // it is assumed that first setting of the bridge occurs together with diff --git a/lib/user.h b/lib/user.h index 0023b44a..7c9ed55f 100644 --- a/lib/user.h +++ b/lib/user.h @@ -105,7 +105,11 @@ namespace QMatrixClient QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; - void processEvent(const RoomMemberEvent& event, const Room* r); + /// This method is for internal use and should not be called + /// from client code + // FIXME: Move it away to private in lib 0.6 + void processEvent(const RoomMemberEvent& event, const Room* r, + bool firstMention); public slots: /** Set a new name in the global user profile */ -- cgit v1.2.3 From 3401eee364d9a41f7f28f2702a4b416a11fb19bc Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 29 Mar 2019 13:30:03 +0900 Subject: Connection: make sure to mark rooms supposed to be direct chats as such Closes #305. Relies on correct tracking of Invite membership from the previous commit. --- lib/connection.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 07c24c92..c09de979 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -609,8 +609,17 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, : QStringLiteral("private"), alias, name, topic, invites, invite3pids, roomVersion, creationContent, initialState, presetName, isDirect); - connect(job, &BaseJob::success, this, [this,job] { - emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); + connect(job, &BaseJob::success, this, [this,job,invites,isDirect] { + auto* room = provideRoom(job->roomId(), JoinState::Join); + if (!room) + { + Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room"); + return; + } + emit createdRoom(room); + if (isDirect) + for (const auto& i: invites) + addToDirectChats(room, user(i)); }); return job; } @@ -1161,6 +1170,9 @@ Room* Connection::provideRoom(const QString& id, Omittable joinState) emit leftRoom(room, prevInvite); if (prevInvite) { + const auto dcUsers = prevInvite->directChatUsers(); + for (auto* u: dcUsers) + addToDirectChats(room, u); qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); -- cgit v1.2.3 From 27c29894a77a0733085b3901297a64773069c61a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 30 Mar 2019 20:52:43 +0900 Subject: User::nameForRoom(): null hint is not a hint This caused the library to erroneously believe that users with no representation in other rooms have no display name even if that display name is provided for the given room. --- lib/user.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/user.cpp b/lib/user.cpp index c373a067..951ad87d 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -82,7 +82,8 @@ class User::Private QString User::Private::nameForRoom(const Room* r, const QString& hint) const { // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedName || otherNames.contains(hint, r)) + if (!hint.isNull() + && (hint == mostUsedName || otherNames.contains(hint, r))) return hint; return otherNames.key(r, mostUsedName); } -- cgit v1.2.3 From 07827998c5ffe495ce83e4b1034d9e016f7296e8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 31 Mar 2019 18:41:12 +0900 Subject: Room::refreshDisplayName() - for debugging purposes only Clients should not need to call this method explicitly. --- lib/room.cpp | 5 +++++ lib/room.h | 3 +++ 2 files changed, 8 insertions(+) diff --git a/lib/room.cpp b/lib/room.cpp index 0305cf7b..b0c898fb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -389,6 +389,11 @@ QString Room::displayName() const return d->displayname; } +void Room::refreshDisplayName() +{ + d->updateDisplayname(); +} + QString Room::topic() const { return d->getCurrentState()->topic(); diff --git a/lib/room.h b/lib/room.h index 3da2d4e7..adec7650 100644 --- a/lib/room.h +++ b/lib/room.h @@ -408,6 +408,9 @@ namespace QMatrixClient void setAliases(const QStringList& aliases); void setTopic(const QString& newTopic); + /// You shouldn't normally call this method; it's here for debugging + void refreshDisplayName(); + void getPreviousContent(int limit = 10); void inviteToRoom(const QString& memberId); -- cgit v1.2.3 From 3f449c8773af6183c14b9c40ff1951a565bc1e67 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 31 Mar 2019 18:42:38 +0900 Subject: Room::updateData(): recalculate room name only when state changes occur --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index b0c898fb..e69d6de1 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1339,7 +1339,6 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) emit memberListChanged(); roomChanges |= d->setSummary(move(data.summary)); - d->updateDisplayname(); for( auto&& ephemeralEvent: data.ephemeral ) roomChanges |= processEphemeralEvent(move(ephemeralEvent)); @@ -1364,6 +1363,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) } if (roomChanges != Change::NoChange) { + d->updateDisplayname(); emit changed(roomChanges); if (!fromCache) connection()->saveRoomState(this); -- cgit v1.2.3 From 7acd18f52da02bcf272b3fe3c8901753bc4adcc9 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 31 Mar 2019 18:44:55 +0900 Subject: Room: track invited users; polish the room naming algorithm It's no more entirely along the spec lines but gives better results with or without lazy-loading, across a wide range of cases. Closes #310. --- lib/room.cpp | 95 +++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index e69d6de1..a20c1cf0 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -105,6 +105,7 @@ class Room::Private members_map_t membersMap; QList usersTyping; QMultiHash eventIdReadUsers; + QList usersInvited; QList membersLeft; int unreadMessages = 0; bool displayed = false; @@ -1224,7 +1225,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u) membersMap.remove(username, u); // If there was one namesake besides the removed user, signal member renaming // for it because it doesn't need to be disambiguated anymore. - // TODO: Think about left users. if (namesake) emit q->memberRenamed(namesake); } @@ -2209,8 +2209,38 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); - if (evt.membership() == MembershipType::Join) + switch (prevMembership) { + case MembershipType::Invite: + if (evt.membership() != prevMembership) + { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + if (evt.membership() == MembershipType::Invite) + qCWarning(MAIN) + << "Invalid membership change from Join to Invite:" + << evt; + if (evt.membership() != prevMembership) + { + d->removeMemberFromMap(u->name(this), u); + emit userRemoved(u); + } + break; + default: + if (evt.membership() == MembershipType::Invite + || evt.membership() == MembershipType::Join) + { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + } + + switch(evt.membership()) + { + case MembershipType::Join: if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); @@ -2226,18 +2256,14 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) }); emit userAdded(u); } - } - else if (evt.membership() != MembershipType::Join) - { - if (prevMembership == MembershipType::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); - } + break; + case MembershipType::Invite: + if (!d->usersInvited.contains(u)) + d->usersInvited.push_back(u); + break; + default: + if (!d->membersLeft.contains(u)) + d->membersLeft.append(u); } return MembersChange; } @@ -2418,42 +2444,59 @@ QString Room::Private::calculateDisplayname() const return dispName; // Using m.room.aliases in naming is explicitly discouraged by the spec - //if (!q->aliases().empty() && !q->aliases().at(0).isEmpty()) - // return q->aliases().at(0); // Supplementary code for 3 and 4: build the shortlist of users whose names // will be used to construct the room name. Takes into account MSC688's // "heroes" if available. + const bool localUserIsIn = joinState == JoinState::Join; const bool emptyRoom = membersMap.isEmpty() || (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); - const auto shortlist = - !summary.heroes.omitted() ? buildShortlist(summary.heroes.value()) : - !emptyRoom ? buildShortlist(membersMap) : - buildShortlist(membersLeft); + const bool nonEmptySummary = + !summary.heroes.omitted() && !summary.heroes->empty(); + auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) : + !emptyRoom ? buildShortlist(membersMap) : + users_shortlist_t { }; + + // When lazy-loading is on, we can rely on the heroes list. + // If it's off, the below code gathers invited and left members. + // NB: including invitations, if any, into naming is a spec extension. + // This kicks in when there's no lazy loading and it's a room with + // the local user as the only member, with more users invited. + if (!shortlist.front() && localUserIsIn) + shortlist = buildShortlist(usersInvited); + + if (!shortlist.front()) // Still empty shortlist; use left members + shortlist = buildShortlist(membersLeft); QStringList names; for (auto u: shortlist) { if (u == nullptr || isLocalUser(u)) break; - names.push_back(q->roomMembername(u)); + // Only disambiguate if the room is not empty + names.push_back(u->displayname(emptyRoom ? nullptr : q)); } - auto usersCountExceptLocal = emptyRoom - ? membersLeft.size() - int(joinState == JoinState::Leave) - : q->joinedCount() - int(joinState == JoinState::Join); + const auto usersCountExceptLocal = + !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) : + !usersInvited.empty() ? usersInvited.count() : + membersLeft.size() - int(joinState == JoinState::Leave); if (usersCountExceptLocal > int(shortlist.size())) names << tr("%Ln other(s)", "Used to make a room name from user names: A, B and _N others_", - usersCountExceptLocal); - auto namesList = QLocale().createSeparatedList(names); + usersCountExceptLocal - int(shortlist.size())); + const auto namesList = QLocale().createSeparatedList(names); // 3. Room members if (!emptyRoom) return namesList; + // (Spec extension) Invited users + if (!usersInvited.empty()) + return tr("Empty room (invited: %1)").arg(namesList); + // 4. Users that previously left the room if (membersLeft.size() > 0) return tr("Empty room (was: %1)").arg(namesList); -- cgit v1.2.3 From 56728b20b227e2e767f103787c394d86b7148843 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 31 Mar 2019 18:45:11 +0900 Subject: CMakeLists.txt: bump the version to 0.5.1 --- CMakeLists.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2d8c218..e3958518 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,7 +146,8 @@ add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_cswellknown_SRCS} ${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS}) set(API_VERSION "0.5") -set_property(TARGET QMatrixClient PROPERTY VERSION "${API_VERSION}.0") +set(FULL_VERSION "${API_VERSION}.1") +set_property(TARGET QMatrixClient PROPERTY VERSION "${FULL_VERSION}") set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} ) set_property(TARGET QMatrixClient PROPERTY INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) @@ -174,10 +175,12 @@ install(DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") include(CMakePackageConfigHelpers) +# NB: SameMajorVersion doesn't really work yet, as we're within 0.x trail. +# Maybe consider jumping the gun and releasing 1.0, as semver advises? write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" - VERSION ${API_VERSION} - COMPATIBILITY AnyNewerVersion + VERSION ${FULL_VERSION} + COMPATIBILITY SameMajorVersion ) export(PACKAGE QMatrixClient) -- cgit v1.2.3 From 27386af703974154256cf755712bb46099500847 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 3 Apr 2019 19:33:24 +0900 Subject: room.h: more doc-comments --- lib/room.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/room.h b/lib/room.h index adec7650..33d1f4ea 100644 --- a/lib/room.h +++ b/lib/room.h @@ -356,8 +356,28 @@ namespace QMatrixClient Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; + + /// Get a file name for downloading for a given event id + /*! + * The event MUST be RoomMessageEvent and have content + * for downloading. \sa RoomMessageEvent::hasContent + */ Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; + + /// Get information on file upload/download + /*! + * \param id uploads are identified by the corresponding event's + * transactionId (because uploads are done before + * the event is even sent), while downloads are using + * the normal event id for identifier. + */ Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + + /// Get the URL to the actual file source in a unified way + /*! + * For uploads it will return a URL to a local file; for downloads + * the URL will be taken from the corresponding room event. + */ Q_INVOKABLE QUrl fileSource(const QString& id) const; /** Pretty-prints plain text into HTML -- cgit v1.2.3 From 7508887564935f70b086ed6fe79bd86757d1fc38 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 3 Apr 2019 20:08:48 +0900 Subject: Room::postFile: initiate uploading the file even before adding a pending event This is to make sure a pending event with file transfer already placed. --- lib/room.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index a20c1cf0..1a63866f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1533,12 +1533,17 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, { QFileInfo localFile { localPath.toLocalFile() }; Q_ASSERT(localFile.isFile()); + + const auto txnId = connection()->generateTxnId(); // Remote URL will only be known after upload; fill in the local path // to enable the preview while the event is pending. - const auto txnId = d->addAsPending(makeEvent( - plainText, localFile, asGenericFile) - )->transactionId(); uploadFile(txnId, localPath); + { + auto&& event = + makeEvent(plainText, localFile, asGenericFile); + event->setTransactionId(txnId); + d->addAsPending(std::move(event)); + } auto* context = new QObject(this); connect(this, &Room::fileTransferCompleted, context, [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { -- cgit v1.2.3 From 1c5696e8f5a9ef87a065e2496eecf178e41e75a7 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 4 Apr 2019 21:27:38 +0900 Subject: Clean up on clang-tidy/clazy analysis --- lib/avatar.cpp | 2 +- lib/connection.cpp | 25 +++++++++++++++---------- lib/connectiondata.cpp | 6 +++--- lib/events/event.cpp | 3 ++- lib/events/stateevent.cpp | 2 +- lib/jobs/basejob.cpp | 2 +- lib/jobs/downloadfilejob.cpp | 5 +++-- lib/jobs/mediathumbnailjob.cpp | 2 +- lib/networkaccessmanager.cpp | 3 ++- lib/networksettings.cpp | 2 +- lib/room.cpp | 32 ++++++++++++++++---------------- lib/settings.cpp | 21 ++++++++++++--------- lib/settings.h | 2 +- lib/syncdata.cpp | 8 ++++---- lib/user.cpp | 14 ++++++++------ lib/util.cpp | 2 +- 16 files changed, 72 insertions(+), 59 deletions(-) diff --git a/lib/avatar.cpp b/lib/avatar.cpp index c0ef3cba..9279ef9d 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -191,7 +191,7 @@ bool Avatar::Private::checkUrl(const QUrl& url) const } QString Avatar::Private::localFile() const { - static const auto cachePath = cacheLocation("avatars"); + static const auto cachePath = cacheLocation(QStringLiteral("avatars")); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } diff --git a/lib/connection.cpp b/lib/connection.cpp index c09de979..5ed72616 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -717,8 +717,8 @@ void Connection::doInDirectChat(User* u, CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { - return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", {}, true); + return createRoom(UnpublishRoom, {}, name, topic, {userId}, + QStringLiteral("trusted_private_chat"), {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) @@ -964,7 +964,8 @@ QHash> Connection::tagsToRooms() const QHash> result; for (auto* r: qAsConst(d->roomMap)) { - for (const auto& tagName: r->tagNames()) + const auto& tagNames = r->tagNames(); + for (const auto& tagName: tagNames) result[tagName].push_back(r); } for (auto it = result.begin(); it != result.end(); ++it) @@ -979,9 +980,12 @@ QStringList Connection::tagNames() const { QStringList tags ({FavouriteTag}); for (auto* r: qAsConst(d->roomMap)) - for (const auto& tag: r->tagNames()) + { + const auto& tagNames = r->tagNames(); + for (const auto& tag: tagNames) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); + } tags.push_back(LowPriorityTag); return tags; } @@ -1264,18 +1268,19 @@ void Connection::saveState() const { QJsonObject rooms; QJsonObject inviteRooms; - for (const auto* i : roomMap()) // Pass on rooms in Leave state + const auto& rs = roomMap(); // Pass on rooms in Leave state + for (const auto* i : rs) (i->joinState() == JoinState::Invite ? inviteRooms : rooms) .insert(i->id(), QJsonValue::Null); QJsonObject roomObj; if (!rooms.isEmpty()) - roomObj.insert("join", rooms); + roomObj.insert(QStringLiteral("join"), rooms); if (!inviteRooms.isEmpty()) - roomObj.insert("invite", inviteRooms); + roomObj.insert(QStringLiteral("invite"), inviteRooms); - rootObj.insert("next_batch", d->data->lastEvent()); - rootObj.insert("rooms", roomObj); + rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); + rootObj.insert(QStringLiteral("rooms"), roomObj); } { QJsonArray accountDataEvents { @@ -1285,7 +1290,7 @@ void Connection::saveState() const accountDataEvents.append( basicEventJson(e.first, e.second->contentJson())); - rootObj.insert("account_data", + rootObj.insert(QStringLiteral("account_data"), QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index eb516ef7..91cda09f 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -25,7 +25,7 @@ using namespace QMatrixClient; struct ConnectionData::Private { - explicit Private(const QUrl& url) : baseUrl(url) { } + explicit Private(QUrl url) : baseUrl(std::move(url)) { } QUrl baseUrl; QByteArray accessToken; @@ -37,7 +37,7 @@ struct ConnectionData::Private }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique(baseUrl)) + : d(std::make_unique(std::move(baseUrl))) { } ConnectionData::~ConnectionData() = default; @@ -98,7 +98,7 @@ QString ConnectionData::lastEvent() const void ConnectionData::setLastEvent(QString identifier) { - d->lastEvent = identifier; + d->lastEvent = std::move(identifier); } QByteArray ConnectionData::generateTxnId() const diff --git a/lib/events/event.cpp b/lib/events/event.cpp index c98dfbb6..6505d89a 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -38,7 +38,8 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) QString EventTypeRegistry::getMatrixType(event_type_t typeId) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : ""; + return typeId < get().eventTypes.size() + ? get().eventTypes[typeId] : QString(); } Event::Event(Type type, const QJsonObject& json) diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index e96614d2..a84f302b 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -27,7 +27,7 @@ using namespace QMatrixClient; RoomEvent::factory_t::addMethod( [] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains("state_key")) + if (!json.contains("state_key"_ls)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index f738ce7a..f521cc4b 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -430,7 +430,7 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) { d->rawResponse = reply->readAll(); - QJsonParseError error; + QJsonParseError error { 0, QJsonParseError::MissingObject }; const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); if( error.error == QJsonParseError::NoError ) return parseJson(json); diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 2bf9dd8f..672a7b2d 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -22,7 +22,8 @@ class DownloadFileJob::Private QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) { - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1)); + return makeRequestUrl( + std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1)); } DownloadFileJob::DownloadFileJob(const QString& serverName, @@ -31,7 +32,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) { - setObjectName("DownloadFileJob"); + setObjectName(QStringLiteral("DownloadFileJob")); } QString DownloadFileJob::targetFileName() const diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index aeb49839..edb9b156 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -59,5 +59,5 @@ BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) if( _thumbnail.loadFromData(data()->readAll()) ) return Success; - return { IncorrectResponseError, "Could not read image data" }; + return { IncorrectResponseError, QStringLiteral("Could not read image data") }; } diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 89967a8a..7d9cb360 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -29,7 +29,8 @@ class NetworkAccessManager::Private QList ignoredSslErrors; }; -NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique()) +NetworkAccessManager::NetworkAccessManager(QObject* parent) + : QNetworkAccessManager(parent), d(std::make_unique()) { } QList NetworkAccessManager::ignoredSslErrors() const diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 48bd09f3..6ff2bc1f 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -27,5 +27,5 @@ void NetworkSettings::setupApplicationProxy() const } QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName) +QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/room.cpp b/lib/room.cpp index 1a63866f..caeeb499 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -168,7 +168,7 @@ class Room::Private //void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); - void renameMember(User* u, QString oldName); + void renameMember(User* u, const QString& oldName); void removeMemberFromMap(const QString& username, User* u); // This updates the room displayname field (which is the way a room @@ -185,7 +185,7 @@ class Room::Private void getPreviousContent(int limit = 10); template - const EventT* getCurrentState(QString stateKey = {}) const + const EventT* getCurrentState(const QString& stateKey = {}) const { static const EventT empty; const auto* evt = @@ -236,8 +236,8 @@ class Room::Private * @param placement - position and direction of insertion: Older for * historical messages, Newer for new ones */ - Timeline::difference_type moveEventsToTimeline(RoomEventsRange events, - EventsPlacement placement); + Timeline::size_type moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement); /** * Remove events from the passed container that are already in the timeline @@ -341,7 +341,7 @@ const QString& Room::id() const QString Room::version() const { const auto v = d->getCurrentState()->version(); - return v.isEmpty() ? "1" : v; + return v.isEmpty() ? QStringLiteral("1") : v; } bool Room::isUnstable() const @@ -546,8 +546,8 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, { const auto oldUnreadCount = unreadMessages; QElapsedTimer et; et.start(); - unreadMessages = count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)); + unreadMessages = int(count_if(eagerMarker, timeline.cend(), + std::bind(&Room::Private::isEventNotable, this, _1))); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; @@ -611,7 +611,7 @@ bool Room::canSwitchVersions() const // TODO, #276: m.room.power_levels const auto* plEvt = - d->currentState.value({"m.room.power_levels", ""}); + d->currentState.value({QStringLiteral("m.room.power_levels"), {}}); if (!plEvt) return true; @@ -621,7 +621,7 @@ bool Room::canSwitchVersions() const .value(localUser()->id()).toInt( plJson.value("users_default"_ls).toInt()); const auto tombstonePowerLevel = - plJson.value("events").toObject() + plJson.value("events"_ls).toObject() .value("m.room.tombstone"_ls).toInt( plJson.value("state_default"_ls).toInt()); return currentUserLevel >= tombstonePowerLevel; @@ -947,7 +947,7 @@ void Room::Private::setTags(TagsMap newTags) } tags = move(newTags); qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with" - << q->tagNames().join(", "); + << q->tagNames().join(QStringLiteral(", ")); emit q->tagsChanged(); } @@ -1196,7 +1196,7 @@ void Room::Private::insertMemberIntoMap(User *u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, QString oldName) +void Room::Private::renameMember(User* u, const QString& oldName) { if (u->name(q) == oldName) { @@ -1234,7 +1234,7 @@ inline auto makeErrorStr(const Event& e, QByteArray msg) return msg.append("; event dump follows:\n").append(e.originalJson()); } -Room::Timeline::difference_type Room::Private::moveEventsToTimeline( +Room::Timeline::size_type Room::Private::moveEventsToTimeline( RoomEventsRange events, EventsPlacement placement) { Q_ASSERT(!events.empty()); @@ -1407,7 +1407,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) return; } it->setDeparted(); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); @@ -1423,7 +1423,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) } it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); } else onEventSendingFailure(txnId); @@ -1442,7 +1442,7 @@ void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString() : tr("The call could not be started")); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } QString Room::retryMessage(const QString& txnId) @@ -2045,7 +2045,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) roomChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); - auto totalInserted = 0; + size_t totalInserted = 0; for (auto it = events.begin(); it != events.end();) { auto nextPendingPair = findFirstOf(it, events.end(), diff --git a/lib/settings.cpp b/lib/settings.cpp index 852e19cb..124d7042 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -84,18 +84,21 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId) -QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) +static const auto HomeserverKey = QStringLiteral("homeserver"); +static const auto AccessTokenKey = QStringLiteral("access_token"); + QUrl AccountSettings::homeserver() const { - return QUrl::fromUserInput(value("homeserver").toString()); + return QUrl::fromUserInput(value(HomeserverKey).toString()); } void AccountSettings::setHomeserver(const QUrl& url) { - setValue("homeserver", url.toString()); + setValue(HomeserverKey, url.toString()); } QString AccountSettings::userId() const @@ -105,19 +108,19 @@ QString AccountSettings::userId() const QString AccountSettings::accessToken() const { - return value("access_token").toString(); + return value(AccessTokenKey).toString(); } void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." " Developers, please save access_token separately."; - setValue("access_token", accessToken); + setValue(AccessTokenKey, accessToken); } void AccountSettings::clearAccessToken() { - legacySettings.remove("access_token"); - legacySettings.remove("device_id"); // Force the server to re-issue it - remove("access_token"); + legacySettings.remove(AccessTokenKey); + legacySettings.remove(QStringLiteral("device_id")); // Force the server to re-issue it + remove(AccessTokenKey); } diff --git a/lib/settings.h b/lib/settings.h index 0b3ecaff..759bda35 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -119,7 +119,7 @@ type classname::propname() const \ \ void classname::setter(type newValue) \ { \ - setValue(QStringLiteral(qsettingname), newValue); \ + setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } \ class AccountSettings: public SettingsGroup diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index f55d4396..21517884 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -72,7 +72,7 @@ void JsonObjectConverter::fillFrom(const QJsonObject& jo, { fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount); fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount); - fromJson(jo["m.heroes"], rs.heroes); + fromJson(jo["m.heroes"_ls], rs.heroes); } template @@ -85,7 +85,7 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, const QJsonObject& room_) : roomId(roomId_) , joinState(joinState_) - , summary(fromJson(room_["summary"])) + , summary(fromJson(room_["summary"_ls])) , state(load(room_, joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) { @@ -121,8 +121,8 @@ 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(); + auto actualVersion = json.value("cache_version"_ls).toObject() + .value("major"_ls).toInt(); if (actualVersion == requiredVersion) parseJson(json, cacheFileInfo.absolutePath() + '/'); else diff --git a/lib/user.cpp b/lib/user.cpp index 951ad87d..17db5760 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -59,7 +59,7 @@ class User::Private QMultiHash otherNames; Avatar mostUsedAvatar { makeAvatar({}) }; std::vector otherAvatars; - auto otherAvatar(QUrl url) + auto otherAvatar(const QUrl& url) { return std::find_if(otherAvatars.begin(), otherAvatars.end(), [&url] (const auto& av) { return av.url() == url; }); @@ -69,7 +69,7 @@ class User::Private mutable int totalRooms = 0; QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, QString oldName); + void setNameForRoom(const Room* r, QString newName, const QString& oldName); QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; void setAvatarForRoom(const Room* r, const QUrl& newUrl, const QUrl& oldUrl); @@ -91,7 +91,7 @@ QString User::Private::nameForRoom(const Room* r, const QString& hint) const static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; void User::Private::setNameForRoom(const Room* r, QString newName, - QString oldName) + const QString& oldName) { Q_ASSERT(oldName != newName); Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); @@ -118,7 +118,8 @@ void User::Private::setNameForRoom(const Room* r, QString newName, et.start(); } - for (auto* r1: connection->roomMap()) + const auto& roomMap = connection->roomMap(); + for (auto* r1: roomMap) if (nameForRoom(r1) == mostUsedName) otherNames.insert(mostUsedName, r1); @@ -178,7 +179,8 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, auto nextMostUsedIt = otherAvatar(newUrl); Q_ASSERT(nextMostUsedIt != otherAvatars.end()); std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->roomMap()) + const auto& roomMap = connection->roomMap(); + for (const auto* r1: roomMap) if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) avatarsToRooms.insert(nextMostUsedIt->url(), r1); @@ -399,7 +401,7 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room, // exceptionally rare (the only reasonable case being that the bridge // changes the naming convention). For the same reason room-specific // bridge tags are not supported at all. - QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); + QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$")); auto match = reSuffix.match(newName); if (match.hasMatch()) { diff --git a/lib/util.cpp b/lib/util.cpp index 8d16cfc8..17674b84 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -156,7 +156,7 @@ static_assert(!is_callable_v>, "Test non-function object"); // "Test returns<> with static member function"); template -QString ft(T&&); +QString ft(T&&) { return {}; } static_assert(std::is_same)>, QString&&>(), "Test function templates"); -- cgit v1.2.3 From 25e94244b4e08d6e5b6eb241076ca0f90816393f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 4 Apr 2019 21:29:57 +0900 Subject: Update README.md and CONTRIBUTING.md (attn: LGPL v3 coming) [skip ci] --- CONTRIBUTING.md | 55 +++++++++++++++++++++++++++++++++---------------------- README.md | 17 ++++++++++++++--- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b534c32..56bc9d91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,17 +86,18 @@ a commit without a DCO is an accident and the DCO still applies. --> ### License -Unless a contributor explicitly specifies otherwise, we assume that all -contributed code is released under [the same license as libQMatrixClient itself](./COPYING), -which is LGPL v2.1 as of the time of this writing. +Unless a contributor explicitly specifies otherwise, we assume contributors +to agree that all contributed code is released either under *LGPL v2.1 or later*. +This is more than just [LGPL v2.1 libQMatrixClient now uses](./COPYING) +because the project plans to switch to LGPL v3 for library code in the near future. Any components proposed for reuse should have a license that permits releasing -a derivative work under LGPL v2.1. Moreover, the license of a proposed component -should be approved by OSI, no exceptions. +a derivative work under *LGPL v2.1 or later* or LGPL v3. Moreover, the license of +a proposed component should be approved by OSI, no exceptions. ## Vulnerability reporting (security issues) @@ -110,7 +111,7 @@ In any of these two options, _indicate that you have such information_ By default, we will give credit to anyone who reports a vulnerability in a responsible way so that we can fix it before public disclosure. If you want -to remain anonymous or pseudonymous instead, please let us know that; we will +to remain anonymous or pseudonymous instead, please let us know; we will gladly respect your wishes. If you provide a fix as a PR, you have no way to remain anonymous (and you also disclose the vulnerability thereby) so this is not the right way, unless the vulnerability is already made public. @@ -156,12 +157,12 @@ The code should strive to be DRY (don't repeat yourself), clear, and obviously c ### Generated C++ code for CS API The code in lib/csapi, lib/identity and lib/application-service, although -it resides in Git, is actually generated from the official Matrix -Swagger/OpenAPI definition files. If you're unhappy with something in these -directories and want to improve the code, you have to understand the way these -files are produced and setup some additional tooling. The shortest possible -procedure resembling the below text can be found in .travis.yml (our Travis CI -configuration actually regenerates those files upon every build). +it resides in Git, is actually generated from (a soft fork of) the official +Matrix Swagger/OpenAPI definition files. If you're unhappy with something in +these directories and want to improve the code, you have to understand the way +these files are produced and setup some additional tooling. The shortest +possible procedure resembling the below text can be found in .travis.yml +(our Travis CI configuration actually regenerates those files upon every build). The generating sequence only works with CMake atm; patches to enable it with qmake are (you guessed it) very welcome. @@ -209,16 +210,23 @@ Instead of relying on the event structure definition in the OpenAPI files, `gtad ### Library API and doc-comments -Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes and if that bothers you. Some parts are not even documented; adding doc-comments to them is highly encouraged. +Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen (with backslashes) style is preferred. You can find that some parts of the code still use JavaDoc (with @'s) style; feel free to replace it with Doxygen backslashes if that bothers you. Some parts are not even documented; adding doc-comments to them is highly encouraged. -Calls, data structures and other symbols not intended for use by clients should _not_ be exposed in (public) .h files, unless they are necessary to declare other public symbols. In particular, this involves private members (functions, typedefs, or variables) in public classes; use pimpl idiom to hide implementation details as much as possible. +Calls, data structures and other symbols not intended for use by clients +should _not_ be exposed in (public) .h files, unless they are necessary +to declare other public symbols. In particular, this involves private members +(functions, typedefs, or variables) in public classes; use pimpl idiom to hide +implementation details as much as possible. `_impl` namespace is reserved for +definitions that should not be used by clients and are not covered by +API guarantees. Note: As of now, all header files of libQMatrixClient are considered public; this may change eventually. ### Qt-flavoured C++ -This is our primary language. We don't have a particular code style _as of yet_ -but some rules-of-thumb are below: +This is our primary language. A particular code style is not enforced _yet_ but +[the PR imposing the common code style](https://github.com/QMatrixClient/libqmatrixclient/pull/295) +is planned to arrive in version 0.6. * 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - we'll thank you for fixing it. * Prefer keeping lines within 80 characters. @@ -260,9 +268,12 @@ but some rules-of-thumb are below: ### Automated tests -There's no testing framework as of now; either Catch or Qt Test or both will be used eventually. However, as a stopgap measure, qmc-example is used for automated end-to-end testing. +There's no testing framework as of now; either Catch or Qt Test or both will +be used eventually. -Any significant addition to the library API should be accompanied by a respective test in qmc-example. To add a test you should: +As a stopgap measure, qmc-example is used for automated functional testing. +Therefore, any significant addition to the library API should be accompanied +by a respective test in qmc-example. To add a test you should: - Add a new private slot to the `QMCTest` class. - Add to the beginning of the slot the line `running.push_back("Test name");`. - Add test logic to the slot, using `QMC_CHECK` macro to assert the test outcome. ALL (even failing) branches should conclude with a QMC_CHECK invocation, unless you intend to have a "DID NOT FINISH" message in the logs under certain conditions. @@ -310,7 +321,7 @@ In Qt Creator, the following line can be used with the Clang code model ### Continuous Integration -We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. Failure on any platform will most likely entail a request to you for a fix before merging a PR. +We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. If your PR fails on any platform double-check that it's not your code causing it - and fix it if it is. ### Other tools @@ -323,7 +334,7 @@ Qt Creator, in addition, knows about clazy, an even deeper Qt-aware static analysis tool. Even level 1 clazy eats away CPU but produces some very relevant and unobvious notices, such as possible unintended copying of a Qt container, or unguarded null pointers. You can use this time to time (see Analyze menu in -Qt Creator) instead of loading your machine with deep runtime analysis. +Qt Creator) instead of hogging your machine with deep analysis as you type. ## Git commit messages @@ -343,7 +354,7 @@ When writing git commit messages, try to follow the guidelines in C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very conservative about adding dependencies to libQMatrixClient. That relates to additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. -Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for automated testing, so PRs onboarding a test framework will be considered with much gratitude. +Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for futures and automated testing, so PRs onboarding those will be considered with much gratitude. Some cases need additional explanation: * Before rolling out your own super-optimised container or algorithm written @@ -367,4 +378,4 @@ Some cases need additional explanation: ## Attribution -This text is largely based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. +This text is based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. diff --git a/README.md b/README.md index ab275a35..857543e1 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,19 @@ You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:mat You can also file issues at [the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). If you have what looks like a security issue, please see respective instructions in CONTRIBUTING.md. ## Building and usage -So far the library is typically used as a git submodule of another project (such as Quaternion); however it can be built separately (either as a static or as a dynamic library). As of version 0.2, the library can be installed and CMake package config files are provided; projects can use `find_package(QMatrixClient)` to setup their code with the installed library files. PRs to enable the same for qmake are most welcome. - -The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for one-off building. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), you're advised to make a recursive check out of that project (in this case, Quaternion) and update the library submodule to its master branch. +So far the library is typically used as a git submodule of another project +(such as Quaternion); however it can be built separately (either as a static or +as a dynamic library). After installing the library the CMake package becomes +available for `find_package(QMatrixClient)` to setup the client code with +the installed library files. PRs to enable the same for qmake are most welcome. + +[The source code is hosted at GitHub](https://github.com/QMatrixClient/libqmatrixclient) - +checking out a certain commit or tag (rather than downloading the archive) is +the recommended way for one-off building. If you want to hack on the library +as a part of another project (e.g. you are working on Quaternion but need +to do some changes to the library code), you're advised to make a recursive +check out of that project (in this case, Quaternion) and update +the library submodule to its master branch. Tags consisting of digits and periods represent released versions; tags ending with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, it is advised to replace a dash with a tilde. @@ -28,6 +38,7 @@ Tags consisting of digits and periods represent released versions; tags ending w - For Ubuntu flavours - zesty or later (or a derivative) is good enough out of the box; older ones will need PPAs at least for a newer Qt; in particular, if you have xenial you're advised to add Kubuntu Backports PPA for it - a Git client to check out this repo - Qt 5 (either Open Source or Commercial), version 5.6 or higher + (5.9 or higher is strongly recommended) - a build configuration tool: - CMake (from your package management system or [the official website](https://cmake.org/download/)) - or qmake (comes with Qt) -- cgit v1.2.3 From edf6a717268d9751f58d256bfe21aab078dfb9f6 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 6 Apr 2019 20:32:09 +0900 Subject: Room::processStateEvent: be more careful with signals handling at user renames --- lib/room.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index caeeb499..9e7ff8d2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1209,7 +1209,6 @@ void Room::Private::renameMember(User* u, const QString& oldName) removeMemberFromMap(oldName, u); insertMemberIntoMap(u); } - emit q->memberRenamed(u); } void Room::Private::removeMemberFromMap(const QString& username, User* u) @@ -2230,6 +2229,8 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) << evt; if (evt.membership() != prevMembership) { + disconnect(u, &User::nameAboutToChange, this, nullptr); + disconnect(u, &User::nameChanged, this, nullptr); d->removeMemberFromMap(u->name(this), u); emit userRemoved(u); } @@ -2257,7 +2258,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) connect(u, &User::nameChanged, this, [=] (QString, QString oldName, const Room* context) { if (context == this) + { d->renameMember(u, oldName); + emit memberRenamed(u); + } }); emit userAdded(u); } -- cgit v1.2.3 From 613e3754dccc56334c6a132381c230781619f264 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 6 Apr 2019 20:55:29 +0900 Subject: CMakeLists.txt: API version should be 0.5.1 instead of 0.5 Also: modernised version setting. --- CMakeLists.txt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e3958518..2b55fc3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.1) -project(qmatrixclient CXX) +set(API_VERSION "0.5.1") # Normally it should just include major.minor +project(qmatrixclient VERSION "${API_VERSION}.1" LANGUAGES CXX) option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) @@ -58,6 +59,7 @@ message( STATUS ) message( STATUS "=============================================================================" ) message( STATUS " libqmatrixclient Build Information" ) message( STATUS "=============================================================================" ) +message( STATUS "Version: ${PROJECT_VERSION}, API version: ${API_VERSION}") if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) @@ -145,9 +147,7 @@ add_library(QMatrixClient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS} ${libqmatrixclient_csdef_SRCS} ${libqmatrixclient_cswellknown_SRCS} ${libqmatrixclient_asdef_SRCS} ${libqmatrixclient_isdef_SRCS}) -set(API_VERSION "0.5") -set(FULL_VERSION "${API_VERSION}.1") -set_property(TARGET QMatrixClient PROPERTY VERSION "${FULL_VERSION}") +set_property(TARGET QMatrixClient PROPERTY VERSION "${PROJECT_VERSION}") set_property(TARGET QMatrixClient PROPERTY SOVERSION ${API_VERSION} ) set_property(TARGET QMatrixClient PROPERTY INTERFACE_QMatrixClient_MAJOR_VERSION ${API_VERSION}) @@ -179,7 +179,6 @@ include(CMakePackageConfigHelpers) # Maybe consider jumping the gun and releasing 1.0, as semver advises? write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/QMatrixClient/QMatrixClientConfigVersion.cmake" - VERSION ${FULL_VERSION} COMPATIBILITY SameMajorVersion ) -- cgit v1.2.3 From 533f837086d3522c54f0622d3f27e0a5d1347b24 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 15 Apr 2019 20:29:00 +0900 Subject: BaseJob: fix a possible crash upon logout See https://github.com/QMatrixClient/Quaternion/issues/566 for details. --- lib/jobs/basejob.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index f521cc4b..a79d0e03 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -600,10 +600,25 @@ QUrl BaseJob::errorUrl() const void BaseJob::setStatus(Status s) { + // The crash that led to this code has been reported in + // https://github.com/QMatrixClient/Quaternion/issues/566 - basically, + // when cleaning up childrent of a deleted Connection, there's a chance + // of pending jobs being abandoned, calling setStatus(Abandoned). + // There's nothing wrong with this; however, the safety check for + // cleartext access tokens below uses d->connection - which is a dangling + // pointer. + // To alleviate that, a stricter condition is applied, that for Abandoned + // and to-be-Abandoned jobs the status message will be disregarded entirely. + // For 0.6 we might rectify the situation by making d->connection + // a QPointer<> (and derive ConnectionData from QObject, respectively). + if (d->status.code == Abandoned || s.code == Abandoned) + s.message.clear(); + if (d->status == s) return; - if (d->connection && !d->connection->accessToken().isEmpty()) + if (!s.message.isEmpty() + && d->connection && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; -- cgit v1.2.3 From 4c9ed0c64ec45c084af657e1fa188242a1d26d1a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 16 Apr 2019 16:21:53 +0900 Subject: BaseJob: preserve the calculated error code if JSON error code is unknown Resetting the code to IncorrectRequestError has been a part of the cause for the incorrect Quaternion behaviour on expired tokens. --- lib/jobs/basejob.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a79d0e03..0d9b9f10 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -334,8 +334,7 @@ void BaseJob::gotReply() tr("Requested room version: %1") .arg(json.value("room_version").toString()); } else if (!json.isEmpty()) // Not localisable on the client side - setStatus(IncorrectRequestError, - json.value("error"_ls).toString()); + setStatus(d->status.code, json.value("error"_ls).toString()); } } -- cgit v1.2.3 From fa41fd7ef8b4706a65de73f8be3e9cff1b8b8014 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 16 Apr 2019 16:22:54 +0900 Subject: Connection::logout: ignore ContentAccessError Closes #316. --- lib/connection.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 5ed72616..fa358e17 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -321,11 +321,14 @@ void Connection::checkAndConnect(const QString& userId, void Connection::logout() { auto job = callApi(); - connect( job, &LogoutJob::success, this, [this] { - stopSync(); - d->data->setToken({}); - emit stateChanged(); - emit loggedOut(); + connect( job, &LogoutJob::finished, this, [job,this] { + if (job->status().good() || job->error() == BaseJob::ContentAccessError) + { + stopSync(); + d->data->setToken({}); + emit stateChanged(); + emit loggedOut(); + } }); } -- cgit v1.2.3 From 97cec65105cab43b95d76b73ebdab74f2e222d81 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 17 Apr 2019 18:53:24 +0900 Subject: Bump the version to 0.5.1.2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b55fc3f..6042267e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1) set(API_VERSION "0.5.1") # Normally it should just include major.minor -project(qmatrixclient VERSION "${API_VERSION}.1" LANGUAGES CXX) +project(qmatrixclient VERSION "${API_VERSION}.2" LANGUAGES CXX) option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) -- cgit v1.2.3 From e3d2edbea279da8c3ed19b9faa77a287b6a65faf Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 18 May 2019 21:48:04 +0900 Subject: event.h: add doc-comments; deprecate ptrCast() --- lib/events/event.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/events/event.h b/lib/events/event.h index d7ac4292..b7bbd83e 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -32,19 +32,23 @@ namespace QMatrixClient template using event_ptr_tt = std::unique_ptr; + /// Unwrap a plain pointer from a smart pointer template - inline EventT* rawPtr(const event_ptr_tt& ptr) // unwrap + inline EventT* rawPtr(const event_ptr_tt& ptr) { return ptr.get(); } + /// Unwrap a plain pointer and downcast it to the specified type template inline TargetEventT* weakPtrCast(const event_ptr_tt& ptr) { return static_cast(rawPtr(ptr)); } + /// Re-wrap a smart pointer to base into a smart pointer to derived template + [[deprecated("Consider using eventCast() or visit() instead")]] inline event_ptr_tt ptrCast(event_ptr_tt&& ptr) { return unique_ptr_cast(ptr); -- cgit v1.2.3 From 92f10dcbc39c9a0762e665ac9acbf4fd8b39f614 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 18 May 2019 21:50:08 +0900 Subject: Connection::onSyncSuccess(): fix using after move() Also rewrite the account data piece with visit(). --- lib/connection.cpp | 116 +++++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index fa358e17..3eede846 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -407,63 +407,67 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { // Let UI update itself after updating each room QCoreApplication::processEvents(); } - for (auto&& accountEvent: data.takeAccountData()) - { - if (is(*accountEvent)) - { - const auto usersToDCs = ptrCast(move(accountEvent)) - ->usersToDirectChats(); - DirectChatsMap removals = - erase_if(d->directChats, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.key()->id(), it.value()); - }); - erase_if(d->directChatUsers, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.value()->id(), it.key()); - }); - if (MAIN().isDebugEnabled()) - for (auto it = removals.begin(); it != removals.end(); ++it) - qCDebug(MAIN) << it.value() - << "is no more a direct chat with" << it.key()->id(); - - DirectChatsMap additions; - for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) - { - if (auto* u = user(it.key())) - { - if (!d->directChats.contains(u, it.value())) - { - Q_ASSERT(!d->directChatUsers.contains(it.value(), u)); - additions.insert(u, it.value()); - d->directChats.insert(u, it.value()); - d->directChatUsers.insert(it.value(), u); - qCDebug(MAIN) << "Marked room" << it.value() + // After running this loop, the account data events not saved in + // d->accountData (see the end of the loop body) are auto-cleaned away + for (auto& eventPtr: data.takeAccountData()) + visit(*eventPtr, + [this](const DirectChatEvent& dce) { + const auto& usersToDCs = dce.usersToDirectChats(); + DirectChatsMap removals = + erase_if(d->directChats, [&usersToDCs](auto it) { + return !usersToDCs.contains(it.key()->id(), + it.value()); + }); + erase_if(d->directChatUsers, [&usersToDCs](auto it) { + return !usersToDCs.contains(it.value()->id(), it.key()); + }); + if (MAIN().isDebugEnabled()) + for (auto it = removals.begin(); it != removals.end(); + ++it) + qCDebug(MAIN) << it.value() + << "is no more a direct chat with" + << it.key()->id(); + + DirectChatsMap additions; + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); + ++it) { + if (auto* u = user(it.key())) { + if (!d->directChats.contains(u, it.value())) { + Q_ASSERT(!d->directChatUsers.contains(it.value(), + u)); + additions.insert(u, it.value()); + d->directChats.insert(u, it.value()); + d->directChatUsers.insert(it.value(), u); + qCDebug(MAIN) + << "Marked room" << it.value() << "as a direct chat with" << u->id(); - } - } else - qCWarning(MAIN) - << "Couldn't get a user object for" << it.key(); - } - if (!additions.isEmpty() || !removals.isEmpty()) - emit directChatsListChanged(additions, removals); - - continue; - } - if (is(*accountEvent)) - qCDebug(MAIN) << "Users ignored by" << d->userId << "updated:" - << QStringList::fromSet(ignoredUsers()).join(','); - - auto& currentData = d->accountData[accountEvent->matrixType()]; - // A polymorphic event-specific comparison might be a bit more - // efficient; maaybe do it another day - if (!currentData || - currentData->contentJson() != accountEvent->contentJson()) - { - currentData = std::move(accountEvent); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); - emit accountDataChanged(currentData->matrixType()); - } - } + } + } else + qCWarning(MAIN) << "Couldn't get a user object for" + << it.key(); + } + if (!additions.isEmpty() || !removals.isEmpty()) + emit directChatsListChanged(additions, removals); + }, + // catch-all, passing eventPtr for a possible take-over + [this, &eventPtr](const Event& accountEvent) { + if (is(accountEvent)) + qCDebug(MAIN) + << "Users ignored by" << d->userId << "updated:" + << QStringList::fromSet(ignoredUsers()).join(','); + + auto& currentData = d->accountData[accountEvent.matrixType()]; + // A polymorphic event-specific comparison might be a bit more + // efficient; maaybe do it another day + if (!currentData + || currentData->contentJson() + != accountEvent.contentJson()) { + currentData = std::move(eventPtr); + qCDebug(MAIN) << "Updated account data of type" + << currentData->matrixType(); + emit accountDataChanged(currentData->matrixType()); + } + }); } void Connection::stopSync() -- cgit v1.2.3 From 432dc68bb0dd4d37542f3fcc2b83309a927362f0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 20 May 2019 07:46:03 +0900 Subject: Connection: Fix a race condition in direct chats handling upon initial sync Closes #323. --- lib/connection.cpp | 129 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 54 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 3eede846..ac69228b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -73,8 +73,7 @@ class Connection::Private : data(move(connection)) { } Q_DISABLE_COPY(Private) - Private(Private&&) = delete; - Private operator=(Private&&) = delete; + DISABLE_MOVE(Private) Connection* q = nullptr; std::unique_ptr data; @@ -91,6 +90,10 @@ class Connection::Private QMap userMap; DirectChatsMap directChats; DirectChatUsersMap directChatUsers; + // The below two variables track local changes between sync completions. + // See also: https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events + DirectChatsMap dcLocalAdditions; + DirectChatsMap dcLocalRemovals; std::unordered_map accountData; QString userId; int syncLoopTimeout = -1; @@ -107,8 +110,6 @@ class Connection::Private void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); - void broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals); template EventT* unpackAccountData() const @@ -373,6 +374,20 @@ void Connection::syncLoop(int timeout) syncLoopIteration(); // initial sync to start the loop } +QJsonObject toJson(const Connection::DirectChatsMap& directChats) +{ + QJsonObject json; + for (auto it = directChats.begin(); it != directChats.end();) + { + QJsonArray roomIds; + const auto* user = it.key(); + for (; it != directChats.end() && it.key() == user; ++it) + roomIds.append(*it); + json.insert(user->id(), roomIds); + } + return json; +} + void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) @@ -409,33 +424,43 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { } // After running this loop, the account data events not saved in // d->accountData (see the end of the loop body) are auto-cleaned away - for (auto& eventPtr: data.takeAccountData()) + for (auto& eventPtr : data.takeAccountData()) + { visit(*eventPtr, [this](const DirectChatEvent& dce) { + // See https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events const auto& usersToDCs = dce.usersToDirectChats(); - DirectChatsMap removals = - erase_if(d->directChats, [&usersToDCs](auto it) { - return !usersToDCs.contains(it.key()->id(), - it.value()); - }); - erase_if(d->directChatUsers, [&usersToDCs](auto it) { - return !usersToDCs.contains(it.value()->id(), it.key()); + DirectChatsMap remoteRemovals = + erase_if(d->directChats, [&usersToDCs, this](auto it) { + return !(usersToDCs.contains(it.key()->id(), it.value()) + || d->dcLocalAdditions.contains(it.key(), + it.value())); + }); + erase_if(d->directChatUsers, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.value(), it.key()); + }); + // Remove from dcLocalRemovals what the server already has. + erase_if(d->dcLocalRemovals, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.key(), it.value()); }); if (MAIN().isDebugEnabled()) - for (auto it = removals.begin(); it != removals.end(); - ++it) - qCDebug(MAIN) << it.value() - << "is no more a direct chat with" - << it.key()->id(); - - DirectChatsMap additions; + for (auto it = remoteRemovals.begin(); + it != remoteRemovals.end(); ++it) { + qCDebug(MAIN) + << it.value() << "is no more a direct chat with" + << it.key()->id(); + } + + DirectChatsMap remoteAdditions; for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { if (auto* u = user(it.key())) { - if (!d->directChats.contains(u, it.value())) { - Q_ASSERT(!d->directChatUsers.contains(it.value(), - u)); - additions.insert(u, it.value()); + if (!d->directChats.contains(u, it.value()) + && !d->dcLocalRemovals.contains(u, it.value())) + { + Q_ASSERT( + !d->directChatUsers.contains(it.value(), u)); + remoteAdditions.insert(u, it.value()); d->directChats.insert(u, it.value()); d->directChatUsers.insert(it.value(), u); qCDebug(MAIN) @@ -446,8 +471,12 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { qCWarning(MAIN) << "Couldn't get a user object for" << it.key(); } - if (!additions.isEmpty() || !removals.isEmpty()) - emit directChatsListChanged(additions, removals); + // Remove from dcLocalAdditions what the server already has. + erase_if(d->dcLocalAdditions, [&remoteAdditions](auto it) { + return remoteAdditions.contains(it.key(), it.value()); + }); + if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) + emit directChatsListChanged(remoteAdditions, remoteRemovals); }, // catch-all, passing eventPtr for a possible take-over [this, &eventPtr](const Event& accountEvent) { @@ -468,6 +497,16 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { emit accountDataChanged(currentData->matrixType()); } }); + } + if (!d->dcLocalAdditions.isEmpty() || !d->dcLocalRemovals.isEmpty()) { + qDebug(MAIN) << "Sending updated direct chats to the server:" + << d->dcLocalRemovals.size() << "removal(s)," + << d->dcLocalAdditions.size() << "addition(s)"; + callApi(d->userId, QStringLiteral("m.direct"), + toJson(d->directChats)); + d->dcLocalAdditions.clear(); + d->dcLocalRemovals.clear(); + } } void Connection::stopSync() @@ -662,8 +701,8 @@ void Connection::doInDirectChat(User* u, { Q_ASSERT(u); const auto& userId = u->id(); - // There can be more than one DC; find the first valid, and delete invalid - // (left/forgotten) ones along the way. + // There can be more than one DC; find the first valid (existing and + // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; for (auto it = d->directChats.find(u); it != d->directChats.end() && it.key() == u; ++it) @@ -700,6 +739,8 @@ void Connection::doInDirectChat(User* u, << roomId << "is not valid and will be discarded"; // Postpone actual deletion until we finish iterating d->directChats. removals.insert(it.key(), it.value()); + // Add to the list of updates to send to the server upon the next sync. + d->dcLocalRemovals.insert(it.key(), it.value()); } if (!removals.isEmpty()) { @@ -709,7 +750,7 @@ void Connection::doInDirectChat(User* u, d->directChatUsers.remove(it.value(), const_cast(it.key())); // FIXME } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } auto j = createDirectChat(userId); @@ -1010,28 +1051,6 @@ Connection::DirectChatsMap Connection::directChats() const return d->directChats; } -QJsonObject toJson(const Connection::DirectChatsMap& directChats) -{ - QJsonObject json; - for (auto it = directChats.begin(); it != directChats.end();) - { - QJsonArray roomIds; - const auto* user = it.key(); - for (; it != directChats.end() && it.key() == user; ++it) - roomIds.append(*it); - json.insert(user->id(), roomIds); - } - return json; -} - -void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals) -{ - q->callApi(userId, QStringLiteral("m.direct"), - toJson(directChats)); - emit q->directChatsListChanged(additions, removals); -} - void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); @@ -1040,8 +1059,8 @@ void Connection::addToDirectChats(const Room* room, User* user) Q_ASSERT(!d->directChatUsers.contains(room->id(), user)); d->directChats.insert(user, room->id()); d->directChatUsers.insert(room->id(), user); - DirectChatsMap additions { { user, room->id() } }; - d->broadcastDirectChatUpdates(additions, {}); + d->dcLocalAdditions.insert(user, room->id()); + emit directChatsListChanged({ { user, room->id() } }, {}); } void Connection::removeFromDirectChats(const QString& roomId, User* user) @@ -1054,15 +1073,17 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user) DirectChatsMap removals; if (user != nullptr) { - removals.insert(user, roomId); d->directChats.remove(user, roomId); d->directChatUsers.remove(roomId, user); + removals.insert(user, roomId); + d->dcLocalRemovals.insert(user, roomId); } else { removals = erase_if(d->directChats, [&roomId] (auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); + d->dcLocalRemovals += removals; } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } bool Connection::isDirectChat(const QString& roomId) const -- cgit v1.2.3 From 681203f951d13e9e8eaf772435cac28c6d74cd42 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Wed, 22 May 2019 08:23:06 +0900 Subject: Version 0.5.2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6042267e..b2536f1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1) set(API_VERSION "0.5.1") # Normally it should just include major.minor -project(qmatrixclient VERSION "${API_VERSION}.2" LANGUAGES CXX) +project(qmatrixclient VERSION "0.5.2" LANGUAGES CXX) option(QMATRIXCLIENT_INSTALL_EXAMPLE "install qmc-example application" ON) -- cgit v1.2.3