diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2018-04-29 18:29:56 +0900 |
---|---|---|
committer | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2018-04-29 18:29:56 +0900 |
commit | b5d962b0f5dcd3497db8a4b3bada46bb666d08e1 (patch) | |
tree | 10d6e3add6b73cf9e4cecdeca08c72b490c50f7a | |
parent | 4ced9792f27ed3d891c1f7772ae30d9fe452dd79 (diff) | |
parent | f5af25428212f139c59941bb294a184242c8b5e0 (diff) | |
download | libquotient-b5d962b0f5dcd3497db8a4b3bada46bb666d08e1.tar.gz libquotient-b5d962b0f5dcd3497db8a4b3bada46bb666d08e1.zip |
Merge branch 'master' into kitsune-gtad
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | lib/avatar.cpp | 44 | ||||
-rw-r--r-- | lib/connection.cpp | 11 | ||||
-rw-r--r-- | lib/connection.h | 6 | ||||
-rw-r--r-- | lib/converters.h | 31 | ||||
-rw-r--r-- | lib/jobs/basejob.cpp | 73 | ||||
-rw-r--r-- | lib/jobs/basejob.h | 4 | ||||
-rw-r--r-- | lib/room.cpp | 51 | ||||
-rw-r--r-- | lib/room.h | 6 | ||||
-rw-r--r-- | lib/user.cpp | 12 | ||||
-rw-r--r-- | lib/user.h | 27 | ||||
-rw-r--r-- | lib/util.h | 28 |
12 files changed, 205 insertions, 90 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 094ec488..b966476f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.5.1 REQUIRED Network Gui) +find_package(Qt5 5.4.1 REQUIRED Network Gui) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) message( STATUS ) diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 1ff2aae1..7e6dc50b 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -1,3 +1,5 @@ +#include <utility> + /****************************************************************************** * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> * @@ -26,12 +28,13 @@ #include <QtCore/QPointer> using namespace QMatrixClient; +using std::move; class Avatar::Private { public: explicit Private(QIcon di, QUrl url = {}) - : _defaultIcon(di), _url(url) + : _defaultIcon(move(di)), _url(move(url)) { } QImage get(Connection* connection, QSize size, get_callback_t callback) const; @@ -51,7 +54,6 @@ class Avatar::Private mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr; mutable QPointer<BaseJob> _uploadRequest = nullptr; mutable std::vector<get_callback_t> callbacks; - mutable get_callback_t uploadCallback; }; Avatar::Avatar(QIcon defaultIcon) @@ -71,13 +73,13 @@ Avatar& Avatar::operator=(Avatar&&) = default; QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const { - return d->get(connection, {dimension, dimension}, callback); + return d->get(connection, {dimension, dimension}, move(callback)); } QImage Avatar::get(Connection* connection, int width, int height, get_callback_t callback) const { - return d->get(connection, {width, height}, callback); + return d->get(connection, {width, height}, move(callback)); } bool Avatar::upload(Connection* connection, const QString& fileName, @@ -85,7 +87,7 @@ bool Avatar::upload(Connection* connection, const QString& fileName, { if (isJobRunning(d->_uploadRequest)) return false; - return d->upload(connection->uploadFile(fileName), callback); + return d->upload(connection->uploadFile(fileName), move(callback)); } bool Avatar::upload(Connection* connection, QIODevice* source, @@ -93,7 +95,7 @@ bool Avatar::upload(Connection* connection, QIODevice* source, { if (isJobRunning(d->_uploadRequest) || !source->isReadable()) return false; - return d->upload(connection->uploadContent(source), callback); + return d->upload(connection->uploadContent(source), move(callback)); } QString Avatar::mediaId() const @@ -104,6 +106,11 @@ QString Avatar::mediaId() const QImage Avatar::Private::get(Connection* connection, QSize size, get_callback_t callback) const { + if (!callback) + { + qCCritical(MAIN) << "Null callbacks are not allowed in Avatar::get"; + Q_ASSERT(false); + } // FIXME: Alternating between longer-width and longer-height requests // is a sure way to trick the below code into constantly getting another // image from the server because the existing one is alleged unsatisfactory. @@ -116,16 +123,19 @@ QImage Avatar::Private::get(Connection* connection, QSize size, _requestedSize = size; if (isJobRunning(_thumbnailRequest)) _thumbnailRequest->abandon(); - callbacks.emplace_back(std::move(callback)); + if (callback) + callbacks.emplace_back(move(callback)); _thumbnailRequest = connection->getThumbnail(_url, size); - QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, [this] - { - _fetched = true; - _originalImage = _thumbnailRequest->scaledThumbnail(_requestedSize); - _scaledImages.clear(); - for (auto n: callbacks) - n(); - }); + QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, + _thumbnailRequest, [this] { + _fetched = true; + _originalImage = + _thumbnailRequest->scaledThumbnail(_requestedSize); + _scaledImages.clear(); + for (const auto& n: callbacks) + n(); + callbacks.clear(); + }); } if( _originalImage.isNull() ) @@ -137,7 +147,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size, _defaultIcon.paint(&p, { QPoint(), _defaultIcon.actualSize(size) }); } - for (auto p: _scaledImages) + for (const auto& p: _scaledImages) if (p.first == size) return p.second; auto result = _originalImage.scaled(size, @@ -151,7 +161,7 @@ bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) _uploadRequest = job; if (!isJobRunning(_uploadRequest)) return false; - _uploadRequest->connect(_uploadRequest, &BaseJob::success, + _uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest, [job,callback] { callback(job->contentUri()); }); return true; } diff --git a/lib/connection.cpp b/lib/connection.cpp index 600ab396..5f930d57 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -84,7 +84,7 @@ class Connection::Private QVector<QString> roomIdsToForget; QMap<QString, User*> userMap; DirectChatsMap directChats; - QHash<QString, QVariantHash> accountData; + QHash<QString, AccountDataMap> accountData; QString userId; SyncJob* syncJob = nullptr; @@ -336,7 +336,7 @@ void Connection::onSyncSuccess(SyncData &&data) { continue; } d->accountData[accountEvent->jsonType()] = - accountEvent->contentJson().toVariantHash(); + fromJson<AccountDataMap>(accountEvent->contentJson()); emit accountDataChanged(accountEvent->jsonType()); } } @@ -590,6 +590,7 @@ Room* Connection::invitation(const QString& roomId) const User* Connection::user(const QString& userId) { + Q_ASSERT(userId.startsWith('@') && userId.contains(':')); if( d->userMap.contains(userId) ) return d->userMap.value(userId); auto* user = userFactory(this, userId); @@ -657,7 +658,7 @@ bool Connection::hasAccountData(const QString& type) const return d->accountData.contains(type); } -QVariantHash Connection::accountData(const QString& type) const +Connection::AccountDataMap Connection::accountData(const QString& type) const { return d->accountData.value(type); } @@ -847,7 +848,7 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 7; +static constexpr int CACHE_VERSION_MAJOR = 8; static constexpr int CACHE_VERSION_MINOR = 0; void Connection::saveState(const QUrl &toFile) const @@ -909,7 +910,7 @@ void Connection::saveState(const QUrl &toFile) const for (auto it = d->accountData.begin(); it != d->accountData.end(); ++it) accountDataEvents.append(QJsonObject { {"type", it.key()}, - {"content", QJsonObject::fromVariantHash(it.value())} + {"content", QMatrixClient::toJson(it.value())} }); rootObj.insert("account_data", QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); diff --git a/lib/connection.h b/lib/connection.h index be414931..839371ef 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -66,6 +66,10 @@ namespace QMatrixClient using DirectChatsMap = QMultiHash<const User*, QString>; + using AccountDataMap = std::conditional_t< + QT_VERSION >= QT_VERSION_CHECK(5, 5, 0), + QVariantHash, QVariantMap>; + enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob explicit Connection(QObject* parent = nullptr); @@ -88,7 +92,7 @@ namespace QMatrixClient * stored on the server. Direct chats map cannot be retrieved * using this method _yet_; use directChats() instead. */ - QVariantHash accountData(const QString& type) const; + AccountDataMap accountData(const QString& type) const; /** Get all Invited and Joined rooms grouped by tag * \return a hashmap from tag name to a vector of room pointers, diff --git a/lib/converters.h b/lib/converters.h index bba298e0..22b22f25 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -21,6 +21,7 @@ #include <QtCore/QJsonObject> #include <QtCore/QJsonArray> // Includes <QtCore/QJsonValue> #include <QtCore/QDate> +#include <QtCore/QVariant> namespace QMatrixClient { @@ -51,6 +52,18 @@ namespace QMatrixClient return QJsonValue(bytes.constData()); } + inline QJsonObject toJson(const QVariantMap& map) + { + return QJsonObject::fromVariantMap(map); + } + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) + inline QJsonObject toJson(const QVariantHash& hMap) + { + return QJsonObject::fromVariantHash(hMap); + } +#endif + template <typename T> inline QJsonObject toJson(const QHash<QString, T>& hashMap) { @@ -163,6 +176,24 @@ namespace QMatrixClient } }; + template <> struct FromJson<QVariantMap> + { + inline auto operator()(const QJsonValue& jv) const + { + return jv.toObject().toVariantMap(); + } + }; + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) + template <> struct FromJson<QVariantHash> + { + inline auto operator()(const QJsonValue& jv) const + { + return jv.toObject().toVariantHash(); + } + }; +#endif + template <typename T> struct FromJson<QHash<QString, T>> { QHash<QString, T> operator()(const QJsonValue& jv) const diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 696861fb..2d41650a 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -336,39 +336,50 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const { - const auto httpCode = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (httpCode == 429) // Qt doesn't know about it yet - return { TooManyRequestsError, tr("Too many requests") }; - - // Should we check httpCode instead? Maybe even use it in BaseJob::Status? - // That would make codes in logs slightly more readable. - switch( reply->error() ) + // QNetworkReply error codes seem to be flawed when it comes to HTTP; + // see, e.g., https://github.com/QMatrixClient/libqmatrixclient/issues/200 + // so check genuine HTTP codes. The below processing is based on + // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + const auto httpCodeHeader = + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + if (!httpCodeHeader.isValid()) // Woah, we didn't even get HTTP headers + return { NetworkError, reply->errorString() }; + + const auto httpCode = httpCodeHeader.toInt(); + if (httpCode / 100 == 2) // 2xx { - case QNetworkReply::NoError: - if (checkContentType(reply->rawHeader("Content-Type"), - d->expectedContentTypes)) - return NoError; - else // A warning in the logs might be more proper instead - return { IncorrectResponseError, - "Incorrect content type of the response" }; - - case QNetworkReply::AuthenticationRequiredError: - case QNetworkReply::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - return { ContentAccessError, reply->errorString() }; - - case QNetworkReply::ProtocolInvalidOperationError: - case QNetworkReply::UnknownContentError: - return { IncorrectRequestError, reply->errorString() }; - - case QNetworkReply::ContentNotFoundError: - return { NotFoundError, reply->errorString() }; - - default: - return { NetworkError, reply->errorString() }; + if (checkContentType(reply->rawHeader("Content-Type"), + d->expectedContentTypes)) + return NoError; + else // A warning in the logs might be more proper instead + return { UnexpectedResponseTypeWarning, + "Unexpected content type of the response" }; } + + return { [httpCode]() -> StatusCode { + if (httpCode / 10 == 41) + return httpCode == 410 ? IncorrectRequestError : NotFoundError; + switch (httpCode) + { + case 401: case 403: case 407: + return ContentAccessError; + case 404: + return NotFoundError; + case 400: case 405: case 406: case 426: case 428: + case 505: + case 494: // Unofficial nginx "Request header too large" + case 497: // Unofficial nginx "HTTP request sent to HTTPS port" + return IncorrectRequestError; + case 429: + return TooManyRequestsError; + case 501: case 510: + return RequestNotImplementedError; + case 511: + return NetworkAuthRequiredError; + default: + return NetworkError; + } + }(), reply->errorString() }; } BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index f243066f..763ef75a 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -53,6 +53,8 @@ namespace QMatrixClient enum StatusCode { NoError = 0 // To be compatible with Qt conventions , Success = 0 , Pending = 1 + , WarningLevel = 20 + , UnexpectedResponseTypeWarning = 21 , Abandoned = 50 //< A very brief period between abandoning and object deletion , ErrorLevel = 100 //< Errors have codes starting from this , NetworkError = 100 @@ -63,6 +65,8 @@ namespace QMatrixClient , IncorrectRequestError , IncorrectResponseError , TooManyRequestsError + , RequestNotImplementedError + , NetworkAuthRequiredError , UserDefinedError = 200 }; diff --git a/lib/room.cpp b/lib/room.cpp index edbc9266..1fa9212e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -109,7 +109,7 @@ class Room::Private QHash<const User*, QString> lastReadEventIds; QString serverReadMarker; TagsMap tags; - QHash<QString, QVariantHash> accountData; + QHash<QString, AccountDataMap> accountData; QString prevBatch; QPointer<RoomMessagesJob> roomMessagesJob; @@ -302,7 +302,8 @@ QImage Room::avatar(int dimension) QImage Room::avatar(int width, int height) { if (!d->avatar.url().isEmpty()) - return d->avatar.get(connection(), width, height, [=] { emit avatarChanged(); }); + return d->avatar.get(connection(), width, height, + [=] { emit avatarChanged(); }); // Use the other side's avatar for 1:1's if (d->membersMap.size() == 2) @@ -637,7 +638,7 @@ bool Room::hasAccountData(const QString& type) const return d->accountData.contains(type); } -QVariantHash Room::accountData(const QString& type) const +Room::AccountDataMap Room::accountData(const QString& type) const { return d->accountData.value(type); } @@ -991,26 +992,32 @@ QString Room::roomMembername(const User* u) const if (username.isEmpty()) return u->id(); - // Get the list of users with the same display name. Most likely, - // there'll be one, but there's a chance there are more. - if (d->membersMap.count(username) == 1) - return username; + auto namesakesIt = qAsConst(d->membersMap).find(username); // We expect a user to be a member of the room - but technically it is // possible to invoke roomMemberName() even for non-members. In such case - // we return the name _with_ id, to stay on a safe side. - // XXX: Causes a storm of false alarms when scrolling through older events - // with left users; commented out until we have a proper backtracking of - // room state ("room time machine"). -// if ( !namesakes.contains(u) ) -// { -// qCWarning() -// << "Room::roomMemberName(): user" << u->id() -// << "is not a member of the room" << id(); -// } + // we return the full name, just in case. + if (namesakesIt == d->membersMap.cend()) + return u->fullName(this); + + auto nextUserIt = namesakesIt + 1; + if (nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) + return username; // No disambiguation necessary + + // Check if we can get away just attaching the bridge postfix + // (extension to the spec) + QVector<QString> bridges; + for (; namesakesIt != d->membersMap.cend() && namesakesIt.key() == username; + ++namesakesIt) + { + const auto bridgeName = (*namesakesIt)->bridged(); + if (bridges.contains(bridgeName)) // Two accounts on the same bridge + return u->fullName(this); // Disambiguate fully + // Don't bother sorting, not so many bridges out there + bridges.push_back(bridgeName); + } - // In case of more than one namesake, use the full name to disambiguate - return u->fullName(this); + return u->rawName(this); // Disambiguate using the bridge postfix only } QString Room::roomMembername(const QString& userId) const @@ -1653,7 +1660,7 @@ void Room::processAccountDataEvent(EventPtr event) } default: d->accountData[event->jsonType()] = - event->contentJson().toVariantHash(); + fromJson<AccountDataMap>(event->contentJson()); emit accountDataChanged(event->jsonType()); } } @@ -1795,7 +1802,7 @@ QJsonObject Room::Private::toJson() const for (const auto *m : membersMap) appendStateEvent(stateEvents, QStringLiteral("m.room.member"), { { QStringLiteral("membership"), QStringLiteral("join") } - , { QStringLiteral("displayname"), m->name(q) } + , { QStringLiteral("displayname"), m->rawName(q) } , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } }, m->id()); @@ -1816,7 +1823,7 @@ QJsonObject Room::Private::toJson() const { for (auto it = accountData.begin(); it != accountData.end(); ++it) appendEvent(accountDataEvents, it.key(), - QJsonObject::fromVariantHash(it.value())); + QMatrixClient::toJson(it.value())); } result.insert("account_data", QJsonObject {{ "events", accountDataEvents }}); @@ -123,6 +123,10 @@ namespace QMatrixClient using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; + using AccountDataMap = std::conditional_t< + QT_VERSION >= QT_VERSION_CHECK(5, 5, 0), + QVariantHash, QVariantMap>; + Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; @@ -269,7 +273,7 @@ namespace QMatrixClient * stored on the server. Tags and read markers cannot be retrieved * using this method _yet_. */ - QVariantHash accountData(const QString& type) const; + AccountDataMap accountData(const QString& type) const; QStringList tagNames() const; TagsMap tags() const; diff --git a/lib/user.cpp b/lib/user.cpp index 6143a061..c4fbfe35 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -220,6 +220,12 @@ QString User::name(const Room* room) const return d->nameForRoom(room); } +QString User::rawName(const Room* room) const +{ + return d->bridged.isEmpty() ? name(room) : + name(room) % " (" % d->bridged % ')'; +} + void User::updateName(const QString& newName, const Room* room) { updateName(newName, d->nameForRoom(room), room); @@ -305,7 +311,7 @@ QString User::displayname(const Room* room) const { auto name = d->nameForRoom(room); return name.isEmpty() ? d->userId : - room ? room->roomMembername(name) : name; + room ? room->roomMembername(this) : name; } QString User::fullName(const Room* room) const @@ -389,12 +395,12 @@ void User::processEvent(RoomMemberEvent* event, const Room* room) // FIXME: the hint doesn't work for bridged users auto oldNameHint = d->nameForRoom(room, event->prevContent()->displayName); - updateName(event->displayName(), oldNameHint, room); + updateName(newName, oldNameHint, room); updateAvatarUrl(event->avatarUrl(), d->avatarUrlForRoom(room, event->prevContent()->avatarUrl), room); } else { - updateName(event->displayName(), room); + updateName(newName, room); updateAvatarUrl(event->avatarUrl(), d->avatarUrlForRoom(room), room); } } @@ -50,24 +50,33 @@ namespace QMatrixClient /** Get the name chosen by the user * This may be empty if the user didn't choose the name or cleared - * it. - * \sa displayName + * it. If the user is bridged, the bridge postfix (such as '(IRC)') + * is stripped out. No disambiguation for the room is done. + * \sa displayName, rawName */ QString name(const Room* room = nullptr) const; + /** Get the user name along with the bridge postfix + * This function is similar to name() but appends the bridge postfix + * (such as '(IRC)') to the user name. No disambiguation is done. + * \sa name, displayName + */ + QString rawName(const Room* room = nullptr) const; + /** Get the displayed user name - * This method returns the result of name() if its non-empty; - * otherwise it returns user id. This is convenient to show a user - * name outside of a room context. In a room context, user names - * should be disambiguated. - * \sa name, id, fullName Room::roomMembername + * When \p room is null, this method returns result of name() if + * the name is non-empty; otherwise it returns user id. + * When \p room is non-null, this call is equivalent to + * Room::roomMembername invocation for the user (i.e. the user's + * disambiguated room-specific name is returned). + * \sa name, id, fullName, Room::roomMembername */ QString displayname(const Room* room = nullptr) const; /** Get user name and id in one string * The constructed string follows the format 'name (id)' - * used for users disambiguation in a room context and in other - * places. + * which the spec recommends for users disambiguation in + * a room context and in other places. * \sa displayName, Room::roomMembername */ QString fullName(const Room* room = nullptr) const; @@ -20,6 +20,7 @@ #include <QtCore/QMetaEnum> #include <QtCore/QDebug> +#include <QtCore/QPointer> #include <functional> #include <memory> @@ -41,6 +42,7 @@ namespace QMatrixClient } #endif + /** static_cast<> for unique_ptr's */ template <typename T1, typename PtrT2> inline auto unique_ptr_cast(PtrT2&& p) { @@ -55,5 +57,31 @@ namespace QMatrixClient template <typename T> static void qAsConst(const T &&) Q_DECL_EQ_DELETE; #endif + + /** 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&&) noexcept = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) noexcept = default; + using QPointer<T>::operator=; + + private: + QObject* subscriber; + }; } // namespace QMatrixClient |