Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libQuotient for openSUSE:Factory checked in at 2026-02-16 13:11:53 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libQuotient (Old) and /work/SRC/openSUSE:Factory/.libQuotient.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libQuotient" Mon Feb 16 13:11:53 2026 rev:23 rq:1333229 version:0.9.6 Changes: -------- --- /work/SRC/openSUSE:Factory/libQuotient/libQuotient.changes 2025-09-20 22:05:37.898574935 +0200 +++ /work/SRC/openSUSE:Factory/.libQuotient.new.1977/libQuotient.changes 2026-02-16 13:18:12.340281435 +0100 @@ -1,0 +2,21 @@ +Sun Feb 15 21:09:13 UTC 2026 - Christophe Marin <[email protected]> + +- Update to 0.9.6 + * Connection object name now actually includes device id once + authentication is done + * emit SSSSHandler::keyBackupUnlocked when receiving backup + key from another device + * Deprecate encryption pickle operations in AccountSettings + * Improving build configuration with libQuotient + * Assume authenticated media API is available before the homeserver + returns versions - helps with avatars sometimes not showing + * Fix potential undefined behaviour when marking messages as read + * Make stable branch compatible with Qt 6.10 + * Add missing hasOlmSession check in sendToDevice + * Connection::join(AndGet)Room(): return the right room object + * Make sure the original messages get hidden when an edit is + available + * Performance improvement for rooms with many leaves + accumulating over time + +------------------------------------------------------------------- Old: ---- libQuotient-0.9.5.tar.gz New: ---- libQuotient-0.9.6.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libQuotient.spec ++++++ --- /var/tmp/diff_new_pack.rc6U4m/_old 2026-02-16 13:18:13.048311664 +0100 +++ /var/tmp/diff_new_pack.rc6U4m/_new 2026-02-16 13:18:13.052311835 +0100 @@ -1,7 +1,7 @@ # # spec file for package libQuotient # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -29,7 +29,7 @@ %define sonum 0.9 %define rname libQuotient Name: libQuotient%{?pkg_suffix} -Version: 0.9.5 +Version: 0.9.6 Release: 0 Summary: Library for Qt Matrix Clients License: LGPL-2.1-only ++++++ libQuotient-0.9.5.tar.gz -> libQuotient-0.9.6.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/.github/workflows/ci.yml new/libQuotient-0.9.6/.github/workflows/ci.yml --- old/libQuotient-0.9.5/.github/workflows/ci.yml 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/.github/workflows/ci.yml 2026-02-15 21:24:53.000000000 +0100 @@ -34,6 +34,8 @@ qt-version: '6.4' - os: windows-latest qt-version: '6.4' + - os: macos-14 + qt-version: '6.10' env: GCC_VERSION: -13 @@ -203,7 +205,7 @@ run: | CTEST_ARGS="--test-dir $BUILD_PATH --output-on-failure" if [[ '${{ runner.os }}' != 'Linux' ]]; then - CTEST_ARGS="$CTEST_ARGS -E testolmaccount" + CTEST_ARGS="$CTEST_ARGS -LE needs_mock_server" else . autotests/setup-tests.sh fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/CMakeLists.txt new/libQuotient-0.9.6/CMakeLists.txt --- old/libQuotient-0.9.5/CMakeLists.txt 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/CMakeLists.txt 2026-02-15 21:24:53.000000000 +0100 @@ -4,7 +4,7 @@ endif() set(API_VERSION "0.9") -project(Quotient VERSION "${API_VERSION}.5" LANGUAGES CXX) +project(Quotient VERSION "${API_VERSION}.6" LANGUAGES CXX) set(PRE_STAGE "") string(JOIN ~ FULL_VERSION ${PROJECT_VERSION} ${PRE_STAGE}) @@ -403,9 +403,12 @@ if (NOT DEFINED BUILD_SHARED_LIBS) set(BUILD_SHARED_LIBS OFF) endif() -configure_file(cmake/${PROJECT_NAME}Config.cmake.in +configure_package_config_file(cmake/${PROJECT_NAME}Config.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/${QUOTIENT_LIB_NAME}/${QUOTIENT_LIB_NAME}Config.cmake" - @ONLY + INSTALL_DESTINATION + "${CMAKE_INSTALL_PREFIX}/${CMakeFilesLocation}" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO ) install(EXPORT ${QUOTIENT_LIB_NAME}Targets diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/connection.cpp new/libQuotient-0.9.6/Quotient/connection.cpp --- old/libQuotient-0.9.5/Quotient/connection.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/connection.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -157,6 +157,7 @@ const QString& deviceId) { d->ensureHomeserver(userId, LoginFlowTypes::Password).then([=, this] { + setObjectName(userId % u"(?)"); d->loginToServer(LoginFlowTypes::Password, makeUserIdentifier(userId), password, /*token*/ QString(), deviceId, initialDeviceName); }); @@ -173,6 +174,7 @@ const QString& deviceId) { Q_ASSERT(d->data->baseUrl().isValid() && d->supportsLoginFlow(LoginFlowTypes::Token)); + setObjectName(loginToken % u"(?)"); d->loginToServer(LoginFlowTypes::Token, std::nullopt /*user is encoded in loginToken*/, QString() /*password*/, loginToken, deviceId, initialDeviceName); } @@ -362,11 +364,9 @@ auto result = promise.future(); promise.start(); if (data->baseUrl().isValid() && (flowType.isEmpty() || supportsLoginFlow(flowType))) { - q->setObjectName(userId % u"(?)"); promise.finish(); // Perfect, we're already good to go } else if (userId.startsWith(u'@') && userId.indexOf(u':') != -1) { // Try to ascertain the homeserver URL and flows - q->setObjectName(userId % u"(?)"); q->resolveServer(userId); if (!flowType.isEmpty()) QtFuture::connect(q, &Connection::loginFlowsChanged) @@ -672,13 +672,13 @@ // If the room object is not there, provideRoom() will create it in Join state. Using // the continuation ensures that the room is provided before any client connections. return callApi<JoinRoomJob>(roomAlias, serverNames, serverNames) - .then([this](const QString& roomId) { provideRoom(roomId); }); + .then([this](const QString& roomId) { provideRoom(roomId, JoinState::Join); }); } QFuture<Room*> Connection::joinAndGetRoom(const QString& roomAlias, const QStringList& serverNames) { return callApi<JoinRoomJob>(roomAlias, serverNames, serverNames) - .then([this](const QString& roomId) { return provideRoom(roomId); }); + .then([this](const QString& roomId) { return provideRoom(roomId, JoinState::Join); }); } QFuture<Room *> Connection::waitForNewRoom(const QString &roomId) @@ -1567,7 +1567,7 @@ const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); - qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; + qCDebug(PROFILER).noquote() << "Cache for" << objectName() << "generated in" << et; outFile.write(data.data(), data.size()); qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); @@ -1855,20 +1855,25 @@ const QString& targetDeviceId, const Event& event, bool encrypted) { - if (encrypted && !d->encryptionData) { + if (!encrypted) { + sendToDevices(event.matrixType(), {{targetUserId, {{targetDeviceId, event.contentJson()}}}}); + return; + } + + if (!d->encryptionData) { qWarning(E2EE) << "E2EE is off for" << objectName() << "- no encrypted to-device message will be sent"; return; } - - const auto contentJson = - encrypted - ? d->encryptionData->assembleEncryptedContent(event.fullJson(), - targetUserId, - targetDeviceId) - : event.contentJson(); - sendToDevices(encrypted ? EncryptedEvent::TypeId : event.matrixType(), - { { targetUserId, { { targetDeviceId, contentJson } } } }); + if (!d->encryptionData->hasOlmSession(targetUserId, targetDeviceId)) { + qWarning(E2EE) << "Olm session for" << targetUserId << '/' << targetDeviceId + << "is missing, to-device message won't be sent"; + return; + } + sendToDevices(EncryptedEvent::TypeId, + {{targetUserId, + {{targetDeviceId, d->encryptionData->assembleEncryptedContent( + event.fullJson(), targetUserId, targetDeviceId)}}}}); } bool Connection::isVerifiedSession(const QByteArray& megolmSessionId) const diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/connection.h new/libQuotient-0.9.6/Quotient/connection.h --- old/libQuotient-0.9.5/Quotient/connection.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/connection.h 2026-02-15 21:24:53.000000000 +0100 @@ -89,7 +89,7 @@ [[deprecated("Compare login flow types instead")]] inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs) { - return !(lhs == rhs); + QT_IGNORE_DEPRECATIONS (return !(lhs == rhs);) } class Connection; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/e2ee/sssshandler.cpp new/libQuotient-0.9.6/Quotient/e2ee/sssshandler.cpp --- old/libQuotient-0.9.5/Quotient/e2ee/sssshandler.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/e2ee/sssshandler.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -148,6 +148,7 @@ Q_ASSERT(m_connection); m_connection->requestKeyFromDevices(MegolmBackupKey).then([this](const QByteArray& key) { loadMegolmBackup(key); + emit keyBackupUnlocked(); }); for (auto k : {CrossSigningUserSigningKey, CrossSigningSelfSigningKey, CrossSigningMasterKey}) m_connection->requestKeyFromDevices(k); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/eventitem.h new/libQuotient-0.9.6/Quotient/eventitem.h --- old/libQuotient-0.9.5/Quotient/eventitem.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/eventitem.h 2026-02-15 21:24:53.000000000 +0100 @@ -23,16 +23,15 @@ * All values except Redacted and Hidden are mutually exclusive. */ enum Code : uint16_t { - Normal = 0x0, ///< No special designation - Submitted = 0x01, ///< The event has just been submitted for sending - 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 - Replaced = 0x10, ///< The event has been replaced - Hidden = 0x100, ///< The event should not be shown in the timeline + Normal = 0x0, //!< No special designation + Submitted = 0x01, //!< The event has just been submitted for sending + 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 + Replaced = 0x10, //!< The event has been replaced + Hidden = 0x100, //!< The event should not be shown in the timeline }; Q_ENUM_NS(Code) } // namespace EventStatus diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/eventcontent.cpp new/libQuotient-0.9.6/Quotient/events/eventcontent.cpp --- old/libQuotient-0.9.5/Quotient/events/eventcontent.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/events/eventcontent.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -133,11 +133,11 @@ const auto relatesTo = fromJson<std::optional<EventRelation>>(json[RelatesToKey]); const auto actualJson = relatesTo.has_value() && relatesTo->type == EventRelation::ReplacementType - ? json.value("m.new_content"_L1).toObject() + ? json.value(NewContentKey).toObject() : json; // Special-casing the custom matrix.org's (actually, Element's) way // of sending HTML messages. - if (actualJson["format"_L1].toString() == HtmlContentTypeId) { + if (actualJson[FormatKey].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; body = actualJson[FormattedBodyKey].toString(); } else { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/eventrelation.h new/libQuotient-0.9.6/Quotient/events/eventrelation.h --- old/libQuotient-0.9.5/Quotient/events/eventrelation.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/events/eventrelation.h 2026-02-15 21:24:53.000000000 +0100 @@ -10,9 +10,16 @@ constexpr inline auto RelatesToKey = "m.relates_to"_L1; constexpr inline auto RelTypeKey = "rel_type"_L1; constexpr inline auto IsFallingBackKey = "is_falling_back"_L1; +constexpr inline auto NewContentKey = "m.new_content"_L1; +constexpr inline auto RelationsKey = "m.relations"_L1; +//! \brief Data about one relation of an event to an upstream event (e.g. a reply or a reaction) +//! +//! This structure contains all information pertaining to a single event relation. In terms of +//! CS API it corresponds to the contents of `m.relates_to` and `m.in_reply_to` JSON objects. struct QUOTIENT_API EventRelation { - using reltypeid_t = QLatin1String; + using typeid_t = QLatin1String; + using reltypeid_t [[deprecated("Use typeid_t")]] = typeid_t; QString type; QString eventId; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/roomevent.cpp new/libQuotient-0.9.6/Quotient/events/roomevent.cpp --- old/libQuotient-0.9.5/Quotient/events/roomevent.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/events/roomevent.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -5,6 +5,9 @@ #include "encryptedevent.h" #include "redactionevent.h" +#include "roomcreateevent.h" +#include "roommemberevent.h" +#include "roompowerlevelsevent.h" #include "stateevent.h" #include "../logging_categories_p.h" @@ -44,6 +47,57 @@ return isRedacted() ? _redactedBecause->reason() : QString {}; } +event_ptr_tt<RoomEvent> RoomEvent::makeRedacted(const RedactionEvent &redaction) const +{ + // The logic below faithfully follows the spec despite quite a few of + // the preserved keys being only relevant for homeservers. Just in case. + static const QStringList TopLevelKeysToKeep{ + EventIdKey, TypeKey, RoomIdKey, SenderKey, + StateKeyKey, ContentKey, "hashes"_L1, "signatures"_L1, + "depth"_L1, "prev_events"_L1, "auth_events"_L1, "origin_server_ts"_L1 + }; + + auto originalJson = this->fullJson(); + for (auto it = originalJson.begin(); it != originalJson.end();) { + if (!TopLevelKeysToKeep.contains(it.key())) + it = originalJson.erase(it); + else + ++it; + } + if (!this->is<RoomCreateEvent>()) { // See MSC2176 on create events + static const QHash<QString, QStringList> ContentKeysToKeepPerType{ + { RedactionEvent::TypeId, { "redacts"_L1 } }, + { RoomMemberEvent::TypeId, + { "membership"_L1, "join_authorised_via_users_server"_L1 } }, + { RoomPowerLevelsEvent::TypeId, + { "ban"_L1, "events"_L1, "events_default"_L1, "invite"_L1, + "kick"_L1, "redact"_L1, "state_default"_L1, "users"_L1, + "users_default"_L1 } }, + // TODO: Replace with RoomJoinRules::TypeId etc. once available + { "m.room.join_rules"_L1, { "join_rule"_L1, "allow"_L1 } }, + { "m.room.history_visibility"_L1, { "history_visibility"_L1 } } + }; + + if (const auto contentKeysToKeep = ContentKeysToKeepPerType.value(this->matrixType()); + !contentKeysToKeep.isEmpty()) { + editSubobject(originalJson, ContentKey, [&contentKeysToKeep](QJsonObject& content) { + for (auto it = content.begin(); it != content.end();) { + if (!contentKeysToKeep.contains(it.key())) + it = content.erase(it); + else + ++it; + } + }); + } else { + originalJson.remove(ContentKey); + originalJson.remove(PrevContentKey); + } + } + replaceSubvalue(originalJson, UnsignedKey, RedactedCauseKey, redaction.fullJson()); + + return loadEvent<RoomEvent>(originalJson); +} + QString RoomEvent::transactionId() const { return unsignedPart<QString>("transaction_id"_L1); @@ -79,6 +133,7 @@ Q_ASSERT(id().isEmpty()); Q_ASSERT(!newId.isEmpty()); editJson().insert(EventIdKey, newId); + _id = newId; qCDebug(EVENTS) << "Event txnId -> id:" << transactionId() << "->" << id(); Q_ASSERT(id() == newId); } @@ -115,3 +170,19 @@ { return containsEventType(StateEvent::BaseMetaType.derivedTypes(), eventTypeId); } + +event_ptr_tt<RoomEvent> RoomEvent::makeReplaced(const RoomEvent &replacement) const +{ + // See https://spec.matrix.org/latest/client-server-api/#applying-mnew_content + auto newContent = replacement.contentPart<QJsonObject>(NewContentKey); + addParam<IfNotEmpty>(newContent, RelatesToKey, this->contentPart<QJsonObject>(RelatesToKey)); + auto originalJson = this->fullJson(); + originalJson.insert(ContentKey, newContent); + editSubobject(originalJson, UnsignedKey, [&replacement](QJsonObject &unsignedData) { + replaceSubvalue(unsignedData, RelationsKey, EventRelation::ReplacementType, + replacement.id()); + }); + + return loadEvent<RoomEvent>(originalJson); +} + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/roomevent.h new/libQuotient-0.9.6/Quotient/events/roomevent.h --- old/libQuotient-0.9.5/Quotient/events/roomevent.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/events/roomevent.h 2026-02-15 21:24:53.000000000 +0100 @@ -5,6 +5,8 @@ #include "event.h" +#include "eventrelation.h" + #include <QtCore/QDateTime> namespace Quotient { @@ -44,6 +46,13 @@ } QString redactionReason() const; + //! \brief Make a redacted event + //! + //! This applies the redaction procedure as defined by the CS API specification to the event's + //! JSON and returns the resulting new event. + //! \note It is the responsibility of the caller to dispose of the original event after that. + event_ptr_tt<RoomEvent> makeRedacted(const RedactionEvent &redaction) const; + //! The transaction_id JSON value for the event. QString transactionId() const; @@ -78,11 +87,22 @@ const EncryptedEvent* originalEvent() const { return _originalEvent.get(); } const QJsonObject encryptedJson() const; + //! \brief Make a replaced event + //! + //! \returns a clone of `*this` with content taken from \p replacement as described in + //! https://spec.matrix.org/latest/client-server-api/#applying-mnew_content + //! \note Disposal of the original event after that is on the caller. + event_ptr_tt<RoomEvent> makeReplaced(const RoomEvent &replacementEvent) const; + protected: explicit RoomEvent(const QJsonObject& json); void dumpTo(QDebug dbg) const override; + virtual void afterRelationChange() {} + private: + QString _id; + // RedactionEvent is an incomplete type here so we cannot inline // constructors using it and also destructors (with 'using', in particular). event_ptr_tt<RedactionEvent> _redactedBecause; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/roommessageevent.cpp new/libQuotient-0.9.6/Quotient/events/roommessageevent.cpp --- old/libQuotient-0.9.5/Quotient/events/roommessageevent.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/events/roommessageevent.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -110,7 +110,7 @@ } newContentJson.insert(BodyKey, plainBody); newContentJson.insert(MsgTypeKey, jsonMsgType); - json.insert("m.new_content"_L1, newContentJson); + json.insert(NewContentKey, newContentJson); json.insert(BodyKey, "* "_L1 + plainBody); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/jobs/downloadfilejob.cpp new/libQuotient-0.9.6/Quotient/jobs/downloadfilejob.cpp --- old/libQuotient-0.9.5/Quotient/jobs/downloadfilejob.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/jobs/downloadfilejob.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -46,7 +46,7 @@ const QString& mediaId) { QT_IGNORE_DEPRECATIONS( // For GetContentJob - return hsData.checkMatrixSpecVersion(u"v1.11") + return hsData.checkMatrixSpecVersion(u"v1.11") || hsData.supportedSpecVersions.empty() // Assume that if we're called early, that the server uses auth media ? GetContentAuthedJob::makeRequestUrl(hsData, serverName, mediaId) : GetContentJob::makeRequestUrl(hsData, serverName, mediaId);) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/jobs/mediathumbnailjob.cpp new/libQuotient-0.9.6/Quotient/jobs/mediathumbnailjob.cpp --- old/libQuotient-0.9.5/Quotient/jobs/mediathumbnailjob.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/jobs/mediathumbnailjob.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -22,7 +22,7 @@ std::optional<bool> animated) { QT_IGNORE_DEPRECATIONS( // For GetContentThumbnailJob - return hsData.checkMatrixSpecVersion(u"v1.11") + return hsData.checkMatrixSpecVersion(u"v1.11") || hsData.supportedSpecVersions.empty() // Assume that if we're called early, that the server uses auth media ? GetContentThumbnailAuthedJob::makeRequestUrl(hsData, serverName, mediaId, requestedSize.width(), requestedSize.height(), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/keyverificationsession.cpp new/libQuotient-0.9.6/Quotient/keyverificationsession.cpp --- old/libQuotient-0.9.5/Quotient/keyverificationsession.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/keyverificationsession.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -204,12 +204,15 @@ EmojiStore loadEmojiStore() { Q_INIT_RESOURCE(libquotientemojis); - QFile dataFile(":/sas-emoji.json"_L1); - dataFile.open(QFile::ReadOnly); - auto data = dataFile.readAll(); - Q_CLEANUP_RESOURCE(libquotientemojis); - return fromJson<EmojiStore>( - QJsonDocument::fromJson(data).array()); + if (QFile dataFile(":/sas-emoji.json"_L1); dataFile.open(QFile::ReadOnly)) { + auto data = dataFile.readAll(); + Q_CLEANUP_RESOURCE(libquotientemojis); + return fromJson<EmojiStore>(QJsonDocument::fromJson(data).array()); + } else { + qCritical(MAIN) + << "Could not open the file with SAS emoji definitions; key verification will not work"; + return {}; + } } EmojiEntry emojiForCode(int code, const QString& language) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/room.cpp new/libQuotient-0.9.6/Quotient/room.cpp --- old/libQuotient-0.9.5/Quotient/room.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/room.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -128,7 +128,7 @@ // For storing a list of current member names for the purpose of disambiguation. QMultiHash<QString, QString> memberNameMap; QStringList membersInvited; - QStringList membersLeft; + QSet<QString> membersLeft; QStringList membersTyping; QHash<QString, QSet<QString>> eventIdReadUsers; @@ -198,6 +198,9 @@ /// A map from event/txn ids to information about the long operation; /// used for both download and upload operations QHash<QString, FileTransferPrivateInfo> fileTransfers; + //! A map of orphans of replacement events that are still looking for their related event in the + //! past. The key is the target event ID, and the value is the replacement event ID. + QHash<QString, QString> orphanedReplacementEvents; const RoomMessageEvent* getEventWithFile(const QString& eventId) const; @@ -207,6 +210,7 @@ const RoomEvent* curEvent); Change processStateEvent(const RoomEvent& curEvent, const RoomEvent* oldEvent); + void processRedactionsAndEdits(RoomEvents &events); void insertMemberIntoMap(const QString& memberId); void removeMemberFromMap(const QString& memberId); @@ -330,7 +334,7 @@ /*! Apply a new revision of the event to the timeline * * Tries to find an event in the timeline and replace it with the new - * content passed in \p newMessage. + * content passed in \p newEvent. * \return true if the event has been found and replaced; false otherwise */ bool processReplacement(const RoomMessageEvent& newEvent); @@ -1059,7 +1063,9 @@ void Room::markAllMessagesAsRead() { - d->markMessagesAsRead(d->timeline.crbegin()); + if (!d->timeline.empty()) { + d->markMessagesAsRead(d->timeline.crbegin()); + } } bool Room::canSwitchVersions() const @@ -1158,13 +1164,13 @@ } const Room::RelatedEvents Room::relatedEvents( - const QString& evtId, EventRelation::reltypeid_t relType) const + const QString& evtId, EventRelation::typeid_t relType) const { return d->relations.value({ evtId, relType }); } const Room::RelatedEvents Room::relatedEvents( - const RoomEvent& evt, EventRelation::reltypeid_t relType) const + const RoomEvent& evt, EventRelation::typeid_t relType) const { return relatedEvents(evt.id(), relType); } @@ -2249,7 +2255,7 @@ QString Room::postPlainText(const QString& plainText) { - return postMessage(plainText, MessageEventType::Text); + return postText<MessageEventType::Text>(plainText); } QString Room::postHtmlMessage(const QString& plainText, const QString& html, @@ -2262,7 +2268,7 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) { - return postHtmlMessage(plainText, html); + return postText<MessageEventType::Text>(plainText, html); } QString Room::postReaction(const QString& eventId, const QString& key) @@ -2276,44 +2282,42 @@ // Remote URL will only be known after upload; fill in the local path // to enable the preview while the event is pending. q->uploadFile(txnId, localUrl); - // Below, the upload job is used as a context object to clean up connections - const auto& transferJob = fileTransfers.value(txnId).job; - connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, - const FileSourceInfo& fileMetadata) { + if (const auto& transferJob = fileTransfers.value(txnId).job) { + // Below, the upload job is used as a context object to clean up connections + connect(q, &Room::fileTransferCompleted, transferJob, + [this, txnId](const QString& tId, const QUrl&, const FileSourceInfo& fileMetadata) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(fileMetadata); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + doSendEvent(it); + } 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" << getUrlFromSourceInfo(fileMetadata) + << "but the event referring to it was " + "cancelled"; + } + }); + connect(q, &Room::fileTransferFailed, transferJob, [this, txnId](const QString& tId) { if (tId != txnId) return; const auto it = q->findPendingEvent(txnId); - if (it != unsyncedEvents.end()) { - it->setFileUploaded(fileMetadata); - emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); - doSendEvent(it); - } 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" << getUrlFromSourceInfo(fileMetadata) - << "but the event referring to it was " - "cancelled"; - } + if (it == unsyncedEvents.end()) + return; + + const auto idx = int(it - unsyncedEvents.begin()); + emit q->pendingEventAboutToDiscard(idx); + // See #286 on why `it` may not be valid here. + unsyncedEvents.erase(unsyncedEvents.begin() + idx); + emit q->pendingEventDiscarded(); }); - connect(q, &Room::fileTransferFailed, transferJob, - [this, txnId](const QString& tId) { - if (tId != txnId) - return; - - const auto it = q->findPendingEvent(txnId); - if (it == unsyncedEvents.end()) - return; - - const auto idx = int(it - unsyncedEvents.begin()); - emit q->pendingEventAboutToDiscard(idx); - // See #286 on why `it` may not be valid here. - unsyncedEvents.erase(unsyncedEvents.begin() + idx); - emit q->pendingEventDiscarded(); - }); + } return txnId; } @@ -2536,16 +2540,24 @@ // This is required because toLocalFile doesn't work on android and toString doesn't work on the desktop auto fileName = localFilename.isLocalFile() ? localFilename.toLocalFile() : localFilename.toString(); FileSourceInfo fileMetadata; + // NB: tempFile needs to live at least until Connection::uploadFile() opens it QTemporaryFile tempFile; if (usesEncryption()) { - tempFile.open(); - QFile file(fileName); - file.open(QFile::ReadOnly); - QByteArray data; - std::tie(fileMetadata, data) = encryptFile(file.readAll()); - tempFile.write(data); - tempFile.close(); - fileName = QFileInfo(tempFile).absoluteFilePath(); + if (tempFile.open()) { + if (QFile file(fileName); file.open(QFile::ReadOnly)) { + QByteArray data; + std::tie(fileMetadata, data) = encryptFile(file.readAll()); + tempFile.write(data); + fileName = QFileInfo(tempFile).absoluteFilePath(); + } else + d->failedTransfer(id, u"Could not open a temporary file for encryption"_s); + + tempFile.close(); + } else + d->failedTransfer(id, u"Could not open the file for uploading"_s); + + if (d->fileTransfers.value(id).status == FileTransferInfo::Failed) + return; } auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobPending(job)) { @@ -2702,64 +2714,6 @@ << "Decrypted" << totalDecrypted << "events in" << et; } -//! \brief Make a redacted event -//! -//! This applies the redaction procedure as defined by the CS API specification -//! to the event's JSON and returns the resulting new event. It is -//! the responsibility of the caller to dispose of the original event after that. -RoomEventPtr makeRedacted(const RoomEvent& target, - const RedactionEvent& redaction) -{ - // The logic below faithfully follows the spec despite quite a few of - // the preserved keys being only relevant for homeservers. Just in case. - static const QStringList TopLevelKeysToKeep{ - EventIdKey, TypeKey, RoomIdKey, SenderKey, - StateKeyKey, ContentKey, "hashes"_L1, "signatures"_L1, - "depth"_L1, "prev_events"_L1, "auth_events"_L1, "origin_server_ts"_L1 - }; - - auto originalJson = target.fullJson(); - for (auto it = originalJson.begin(); it != originalJson.end();) { - if (!TopLevelKeysToKeep.contains(it.key())) - it = originalJson.erase(it); - else - ++it; - } - if (!target.is<RoomCreateEvent>()) { // See MSC2176 on create events - static const QHash<QString, QStringList> ContentKeysToKeepPerType{ - { RedactionEvent::TypeId, { "redacts"_L1 } }, - { RoomMemberEvent::TypeId, - { "membership"_L1, "join_authorised_via_users_server"_L1 } }, - { RoomPowerLevelsEvent::TypeId, - { "ban"_L1, "events"_L1, "events_default"_L1, "invite"_L1, - "kick"_L1, "redact"_L1, "state_default"_L1, "users"_L1, - "users_default"_L1 } }, - // TODO: Replace with RoomJoinRules::TypeId etc. once available - { "m.room.join_rules"_L1, { "join_rule"_L1, "allow"_L1 } }, - { "m.room.history_visibility"_L1, { "history_visibility"_L1 } } - }; - - if (const auto contentKeysToKeep = ContentKeysToKeepPerType.value(target.matrixType()); - !contentKeysToKeep.isEmpty()) // - { - editSubobject(originalJson, ContentKey, [&contentKeysToKeep](QJsonObject& content) { - for (auto it = content.begin(); it != content.end();) { - if (!contentKeysToKeep.contains(it.key())) - it = content.erase(it); - else - ++it; - } - }); - } else { - originalJson.remove(ContentKey); - originalJson.remove(PrevContentKey); - } - } - replaceSubvalue(originalJson, UnsignedKey, RedactedCauseKey, redaction.fullJson()); - - return loadEvent<RoomEvent>(originalJson); -} - bool Room::Private::processRedaction(const RedactionEvent& redaction) { // Can't use findInTimeline because it returns a const iterator, and @@ -2781,7 +2735,7 @@ // 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)); + auto oldEvent = ti.replaceEvent(ti->makeRedacted(redaction)); qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { // Check whether the old event was a part of current state; if it was, @@ -2815,27 +2769,11 @@ return true; } -/** Make a replaced event - * - * Takes \p target and returns a copy of it with content taken from - * \p replacement. Disposal of the original event after that is on the caller. - */ -RoomEventPtr makeReplaced(const RoomEvent& target, - const RoomMessageEvent& replacement) -{ - auto newContent = replacement.contentPart<QJsonObject>("m.new_content"_L1); - addParam<IfNotEmpty>(newContent, RelatesToKey, target.contentPart<QJsonObject>(RelatesToKey)); - auto originalJson = target.fullJson(); - originalJson[ContentKey] = newContent; - editSubobject(originalJson, UnsignedKey, [&replacement](QJsonObject& unsignedData) { - replaceSubvalue(unsignedData, "m.relations"_L1, "m.replace"_L1, replacement.id()); - }); - - return loadEvent<RoomEvent>(originalJson); -} - -bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) +bool Room::Private::processReplacement(const RoomMessageEvent &newEvent) { + if (QUO_ALARM_X(newEvent.isStateEvent(), "Replacing state events is not allowed")) + return false; + // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. const auto pIdx = eventsIndex.constFind(newEvent.replacedEvent()); @@ -2847,7 +2785,7 @@ auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; const auto* const rme = ti.viewAs<RoomMessageEvent>(); if (!rme) { - qCWarning(STATE) << "Ignoring attempt to replace a non-message event" + qCWarning(STATE) << "Replacing a non-message event is implemented in libQuotient 0.10" << ti->id(); return false; } @@ -2859,7 +2797,7 @@ // 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(makeReplaced(*ti, newEvent)); + auto oldEvent = ti.replaceEvent(ti->makeReplaced(newEvent)); qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), std::to_address(oldEvent)); return true; @@ -2896,19 +2834,6 @@ emit q->updatedEvent(content.eventId); } -namespace { -/// Whether the event is a redaction or a replacement -inline bool isEditing(const RoomEventPtr& ep) -{ - return QUO_CHECK(ep != nullptr) - && ep->switchOnType([](const RedactionEvent&) { return true; }, - [](const RoomMessageEvent& rme) { - return !rme.replacedEvent().isEmpty(); - }, - false); -} -} - Room::Timeline::size_type Room::Private::mergePendingEvent(PendingEvents::iterator localEchoIt, RoomEvents::iterator remoteEchoIt) { @@ -2944,50 +2869,11 @@ return Change::None; decryptIncomingEvents(events); + processRedactionsAndEdits(events); QElapsedTimer et; et.start(); - { - using namespace std::ranges; - // Pre-process redactions and edits so that events that get - // redacted/replaced in the same batch landed in the timeline already - // treated. - // NB: We have to store redacting/replacing events to the timeline too - - // see #220. - auto it = find_if(events, isEditing); - for (const auto& eptr : subrange(it, events.end())) { - if (auto* r = eventCast<RedactionEvent>(eptr)) { - // Try to find the target in the timeline, then in the batch. - if (processRedaction(*r)) - continue; - if (auto targetIt = find(events, r->redactedEvent(), &RoomEvent::id); - targetIt != events.end()) - *targetIt = makeRedacted(**targetIt, *r); - else - qCDebug(STATE) - << "Redaction" << r->id() << "ignored: target event" - << r->redactedEvent() << "is not found"; - // If the target event comes later, it comes already redacted. - } - if (auto* msg = eventCast<RoomMessageEvent>(eptr); - msg && !msg->replacedEvent().isEmpty()) { - if (processReplacement(*msg)) - continue; - if (auto targetIt = find(events.begin(), it, msg->replacedEvent(), &RoomEvent::id); - targetIt != it) - *targetIt = makeReplaced(**targetIt, *msg); - else // FIXME: hide the replacing event when target arrives later - qCDebug(EVENTS) - << "Replacing event" << msg->id() - << "ignored: target event" << msg->replacedEvent() - << "is not found"; - // Same as with redactions above, the replaced event coming - // later will come already with the new content. - } - } - } - // 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 @@ -3100,6 +2986,7 @@ const auto timelineSize = timeline.size(); decryptIncomingEvents(events); + processRedactionsAndEdits(events); QElapsedTimer et; et.start(); @@ -3174,7 +3061,7 @@ case Membership::Leave: if (rme.membership() == Membership::Invite || rme.membership() == Membership::Join) { - membersLeft.removeOne(rme.userId()); + membersLeft.remove(rme.userId()); Q_ASSERT(!membersLeft.contains(rme.userId())); } break; @@ -3290,8 +3177,7 @@ case Membership::Knock: case Membership::Ban: case Membership::Leave: - if (!membersLeft.contains(evt.userId())) - membersLeft.append(evt.userId()); + membersLeft.insert(evt.userId()); break; case Membership::Undefined: qCWarning(MEMBERS) << "Ignored undefined membership type"; @@ -3314,6 +3200,89 @@ Change::Other); } +void Room::Private::processRedactionsAndEdits(RoomEvents &events) +{ + using namespace std::ranges; + // Pre-process redactions and edits so that events that get redacted/replaced in the same batch + // landed in the timeline already treated. + // NB: We have to store redacting/replacing events to the timeline too - see #220. + for (auto &evt : events) { + if (const auto *r = eventCast<RedactionEvent>(evt)) { + // Try to find the target in the timeline + if (processRedaction(*r)) + continue; + // Otherwise, check this batch for the event + if (auto targetIt = find(events, r->redactedEvent(), &RoomEvent::id); + targetIt != events.end()) + *targetIt = (*targetIt)->makeRedacted(*r); + else + qCDebug(STATE) << "Redaction" << r->id() << "ignored: target event" + << r->redactedEvent() << "is not found"; + // If the target event comes later, we expected it to come already redacted + continue; + // We don't expect redaction events to participate in edits, although The Spec doesn't + // say anything about it + } + + const auto* const rme = eventCast<const RoomMessageEvent>(evt); + // Check if we have orphaned replacements for the event just received; if yes, edit it + if (const auto replacementEventId = orphanedReplacementEvents.take(evt->id()); + !replacementEventId.isEmpty()) { + // Check the replacement validity (see + // https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events), + // with a caveat that 0.9 can only deal with message event replacements because of + // back-comp; if the replacement is invalid, the old event will remain intact and the + // replacement we now know is invalid will be hidden anyway + if (!rme) [[unlikely]] + qDebug(EVENTS).noquote() << "This version of libQuotient doesn't support " + "replacement of non-message events, skipping" + << evt->id(); + else if (rme->relatesTo()) [[unlikely]] + qDebug(EVENTS).noquote() << "Attempt to replace event" << rme->id() + << "that is itself a replacement - skipping"; + else if (const auto replacementIt = q->findInTimeline(replacementEventId); + replacementIt != historyEdge()) { + if (evt->metaType() != (*replacementIt)->metaType()) [[unlikely]] + qCDebug(EVENTS).noquote() + << "Attempt to replace" << evt->matrixType() << "with" + << (*replacementIt)->matrixType() << "- skipping"; + else if (evt->senderId() != (*replacementIt)->senderId()) [[unlikely]] + qCDebug(EVENTS).noquote() + << "Attempt to replace an event from" << evt->senderId() + << "with one from" << (*replacementIt)->senderId() << "- skipping"; + else { + qCDebug(EVENTS) << "Found parent" << evt->id() << "for replacement event" + << (*replacementIt)->id(); + evt = evt->makeReplaced(**replacementIt); + } + } + continue; + } + + if (rme) { + // Check if the event is a replacement, which we hopefully have the target for; if we + // don't then we store it in orphaned replacements and postpone any action until + // the original event becomes known + if (const auto replacedEventId = rme->replacedEvent(); !replacedEventId.isEmpty()) { + // Try to find the target in the timeline + if (processReplacement(*rme)) + continue; + // Otherwise, check the current batch for the event + if (auto targetIt = find(events, replacedEventId, &RoomEvent::id); + targetIt != events.end()) + *targetIt = (*targetIt)->makeReplaced(*evt); + else if (evt->isStateEvent()){ + qCDebug(EVENTS) + << "Processing of replacement event" << evt->id() + << "postponed: target event" << rme->replacedEvent() + << "is not found, will check for it in older batches when they come"; + orphanedReplacementEvents[rme->replacedEvent()] = evt->id(); + } + } + } + } +} + Room::Changes Room::processEphemeralEvent(EventPtr&& event) { Changes changes {}; @@ -3468,7 +3437,7 @@ shortlist = buildShortlist(membersInvited); if (shortlist.front().isEmpty()) - shortlist = buildShortlist(membersLeft); + shortlist = buildShortlist(membersLeft.values()); QStringList names; for (const auto& u : shortlist) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/room.h new/libQuotient-0.9.6/Quotient/room.h --- old/libQuotient-0.9.5/Quotient/room.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/room.h 2026-02-15 21:24:53.000000000 +0100 @@ -389,9 +389,9 @@ PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; const RelatedEvents relatedEvents(const QString& evtId, - EventRelation::reltypeid_t relType) const; + EventRelation::typeid_t relType) const; const RelatedEvents relatedEvents(const RoomEvent& evt, - EventRelation::reltypeid_t relType) const; + EventRelation::typeid_t relType) const; const RoomCreateEvent* creation() const; const RoomTombstoneEvent* tombstone() const; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/settings.h new/libQuotient-0.9.6/Quotient/settings.h --- old/libQuotient-0.9.5/Quotient/settings.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/Quotient/settings.h 2026-02-15 21:24:53.000000000 +0100 @@ -154,7 +154,9 @@ QUrl homeserver() const; void setHomeserver(const QUrl& url); + [[deprecated("Client code shouldn't use the pickle; and the library stores it in a keychain")]] QByteArray encryptionAccountPickle(); + [[deprecated("Client code shouldn't use the pickle; and the library stores it in a keychain")]] void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle); Q_INVOKABLE void clearEncryptionAccountPickle(); }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/autotests/CMakeLists.txt new/libQuotient-0.9.6/autotests/CMakeLists.txt --- old/libQuotient-0.9.5/autotests/CMakeLists.txt 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/autotests/CMakeLists.txt 2026-02-15 21:24:53.000000000 +0100 @@ -9,16 +9,19 @@ add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) function(QUOTIENT_ADD_TEST) - cmake_parse_arguments(ARG "" "NAME" "" ${ARGN}) + cmake_parse_arguments(ARG "NEEDS_MOCK_SERVER" "NAME" "" ${ARGN}) add_executable(${ARG_NAME} ${ARG_NAME}.cpp testutils.h testutils.cpp) target_link_libraries(${ARG_NAME} ${Qt}::Core ${Qt}::Test ${QUOTIENT_LIB_NAME}) add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME}) add_dependencies(autotests ${ARG_NAME}) + if (ARG_NEEDS_MOCK_SERVER) + set_property(TEST ${ARG_NAME} APPEND PROPERTY LABELS needs_mock_server) + endif() endfunction() quotient_add_test(NAME callcandidateseventtest) quotient_add_test(NAME utiltests) -quotient_add_test(NAME testolmaccount) +quotient_add_test(NAME testolmaccount NEEDS_MOCK_SERVER) quotient_add_test(NAME testgroupsession) quotient_add_test(NAME testolmsession) if (NOT MSVC) @@ -30,4 +33,5 @@ quotient_add_test(NAME testcrosssigning) quotient_add_test(NAME testkeyimport) quotient_add_test(NAME testsettings) -quotient_add_test(NAME testthread) +quotient_add_test(NAME testthread NEEDS_MOCK_SERVER) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/autotests/testcrosssigning.cpp new/libQuotient-0.9.6/autotests/testcrosssigning.cpp --- old/libQuotient-0.9.5/autotests/testcrosssigning.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/autotests/testcrosssigning.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -20,7 +20,7 @@ path = path.left(path.lastIndexOf(QDir::separator())); path += "/cross_signing_data.json"_L1; QFile file(path); - file.open(QIODevice::ReadOnly); + QVERIFY(file.open(QIODevice::ReadOnly)); auto data = file.readAll(); QVERIFY(!data.isEmpty()); auto jobMock = Mocked<QueryKeysJob>(QHash<QString, QStringList>{}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/autotests/testkeyimport.cpp new/libQuotient-0.9.6/autotests/testkeyimport.cpp --- old/libQuotient-0.9.5/autotests/testkeyimport.cpp 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/autotests/testkeyimport.cpp 2026-02-15 21:24:53.000000000 +0100 @@ -25,7 +25,7 @@ path = path.left(path.lastIndexOf(QDir::separator())); path += "/key-export.data"_L1; QFile file(path); - file.open(QIODevice::ReadOnly); + QVERIFY(file.open(QIODevice::ReadOnly)); auto data = file.readAll(); QVERIFY(!data.isEmpty()); const auto result = keyImport.decrypt(QString::fromUtf8(data), u"123passphrase"_s); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/autotests/testutils.h new/libQuotient-0.9.6/autotests/testutils.h --- old/libQuotient-0.9.5/autotests/testutils.h 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/autotests/testutils.h 2026-02-15 21:24:53.000000000 +0100 @@ -25,10 +25,10 @@ inline event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName) { if (!eventFileName.isEmpty()) { - QFile testEventFile; - testEventFile.setFileName(QLatin1StringView(DATA_DIR) + u'/' + eventFileName); - testEventFile.open(QIODevice::ReadOnly); - return loadEvent<EventT>(QJsonDocument::fromJson(testEventFile.readAll()).object()); + if (QFile testEventFile(QStringLiteral(DATA_DIR "/") + eventFileName); + testEventFile.open(QIODevice::ReadOnly)) { + return loadEvent<EventT>(QJsonDocument::fromJson(testEventFile.readAll()).object()); + } } return nullptr; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libQuotient-0.9.5/cmake/QuotientConfig.cmake.in new/libQuotient-0.9.6/cmake/QuotientConfig.cmake.in --- old/libQuotient-0.9.5/cmake/QuotientConfig.cmake.in 2025-09-20 13:57:20.000000000 +0200 +++ new/libQuotient-0.9.6/cmake/QuotientConfig.cmake.in 2026-02-15 21:24:53.000000000 +0100 @@ -1,5 +1,11 @@ +@PACKAGE_INIT@ + include(CMakeFindDependencyMacro) +find_dependency(@Qt@Core) +if (@Qt@Core_VERSION VERSION_GREATER_EQUAL 6.10) + find_dependency(@Qt@CorePrivate) +endif() find_dependency(@Qt@Gui) find_dependency(@Qt@Network) find_dependency(@Qt@Keychain) @@ -7,6 +13,10 @@ find_dependency(OpenSSL) find_dependency(@Qt@Sql) +if(NOT @BUILD_SHARED_LIBS@ AND @Qt@Core_VERSION VERSION_GREATER_EQUAL 6.10) + find_dependency(@Qt@CorePrivate) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]") if (NOT QUOTIENT_FORCE_NAMESPACED_INCLUDES)
