diff options
-rw-r--r-- | CMakeLists.txt | 6 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | examples/qmc-example.cpp | 193 | ||||
-rw-r--r-- | lib/connection.h | 21 | ||||
-rw-r--r-- | lib/events/stateevent.cpp | 10 | ||||
-rw-r--r-- | lib/qt_connection_util.h | 107 | ||||
-rw-r--r-- | lib/util.h | 33 | ||||
-rw-r--r-- | libqmatrixclient.pri | 1 |
8 files changed, 246 insertions, 126 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c48a7ba9..f0f8ac5a 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) @@ -200,7 +202,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) @@ -63,6 +63,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: diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 8fbf4824..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 <QtCore/QCoreApplication> #include <QtCore/QStringBuilder> @@ -34,14 +35,14 @@ class QMCTest : public QObject void sendFile(); void checkFileSendingOutcome(const QString& txnId, const QString& fileName); + void setTopic(); 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); - void leave(); + void conclude(); void finalize(); private: @@ -95,7 +96,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 +111,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 +150,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(); }); } @@ -183,6 +170,7 @@ void QMCTest::doTests() sendMessage(); sendFile(); + setTopic(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); @@ -230,21 +218,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<RoomMessageEvent>(*evt) && !evt->id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() == evt->transactionId()); + return true; }); } @@ -275,30 +261,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); @@ -325,18 +307,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", @@ -349,6 +329,47 @@ void QMCTest::checkFileSendingOutcome(const QString& txnId, [this] (const RoomEvent&) { 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; }); } @@ -370,7 +391,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; @@ -394,41 +415,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() @@ -469,13 +489,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 % ": <strong><font data-mx-color='#" % color % + "' color='#" % color % "'>Testing complete</font></strong>, " % + succeededRec; + if (!failed.isEmpty()) + { + plainReport += "\nFAILED: " % failed.join(", "); + htmlReport += "<br><strong>Failed:</strong> " % failed.join(", "); + } + if (!running.isEmpty()) + { + plainReport += "\nDID NOT FINISH: " % running.join(", "); + htmlReport += + "<br><strong>Did not finish:</strong> " % 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 +541,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()); }); diff --git a/lib/connection.h b/lib/connection.h index cba57e3d..9e4121f4 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 <QtCore/QObject> #include <QtCore/QUrl> @@ -50,26 +51,6 @@ namespace QMatrixClient class SendMessageJob; class LeaveRoomJob; - /** Create a single-shot connection that triggers on the signal and - * then self-disconnects - * - * Only supports DirectConnection type. - */ - template <typename SenderT1, typename SignalT, - typename ReceiverT2, typename SlotT> - 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<Room*(Connection*, const QString&, 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<StateEventBase>(unknownEventTypeId(), json) - : nullptr; + + return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); bool StateEventBase::repeatsState() const diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h new file mode 100644 index 00000000..c2bde8df --- /dev/null +++ b/lib/qt_connection_util.h @@ -0,0 +1,107 @@ +/****************************************************************************** + * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "util.h" + +#include <QtCore/QPointer> + +namespace QMatrixClient { + namespace _impl { + template <typename SenderT, typename SignalT, + typename ContextT, typename... ArgTs> + inline QMetaObject::Connection connectUntil( + SenderT* sender, SignalT signal, ContextT* context, + std::function<bool(ArgTs...)> 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<QMetaObject::Connection>(); +#else + auto pc = std::make_unique<QMetaObject::Connection>(); +#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); // If it's been triggered, it should exist + if (slot(std::forward<ArgTs>(args)...)) + QObject::disconnect(*pc); + }, connType); + return c; + } + } + + template <typename SenderT, typename SignalT, + typename ContextT, typename FunctorT> + 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<FunctorT>::function_type(slot), + connType); + } + + /** Create a single-shot connection that triggers on the signal and + * then self-disconnects + * + * Only supports DirectConnection type. + */ + template <typename SenderT, typename SignalT, + typename ReceiverT, typename SlotT> + 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 <typename T> + class ConnectionsGuard : public QPointer<T> + { + public: + ConnectionsGuard(T* publisher, QObject* subscriber) + : QPointer<T>(publisher), subscriber(subscriber) + { } + ~ConnectionsGuard() + { + if (*this) + (*this)->disconnect(subscriber); + } + ConnectionsGuard(ConnectionsGuard&&) = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) = default; + Q_DISABLE_COPY(ConnectionsGuard) + using QPointer<T>::operator=; + + private: + QObject* subscriber; + }; +} @@ -18,8 +18,9 @@ #pragma once -#include <QtCore/QPointer> -#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0)) +#include <QtCore/QLatin1String> + +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) #include <QtCore/QMetaEnum> #include <QtCore/QDebug> #endif @@ -185,6 +186,7 @@ namespace QMatrixClient static constexpr auto is_callable = true; using return_type = ReturnT; using arg_types = std::tuple<ArgTs...>; + using function_type = std::function<ReturnT(ArgTs...)>; static constexpr auto arg_number = std::tuple_size<arg_types>::value; }; @@ -284,33 +286,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 <typename T> - class ConnectionsGuard : public QPointer<T> - { - public: - ConnectionsGuard(T* publisher, QObject* subscriber) - : QPointer<T>(publisher), subscriber(subscriber) - { } - ~ConnectionsGuard() - { - if (*this) - (*this)->disconnect(subscriber); - } - ConnectionsGuard(ConnectionsGuard&&) = default; - ConnectionsGuard& operator=(ConnectionsGuard&&) = default; - Q_DISABLE_COPY(ConnectionsGuard) - using QPointer<T>::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 \ |