diff options
-rw-r--r-- | CMakeLists.txt | 6 | ||||
-rw-r--r-- | connection.cpp | 103 | ||||
-rw-r--r-- | connection.h | 24 | ||||
-rw-r--r-- | jobs/converters.h | 89 | ||||
-rw-r--r-- | jobs/generated/inviting.cpp | 37 | ||||
-rw-r--r-- | jobs/generated/inviting.h | 42 | ||||
-rw-r--r-- | jobs/generated/kicking.cpp | 41 | ||||
-rw-r--r-- | jobs/generated/kicking.h | 45 | ||||
-rw-r--r-- | jobs/syncjob.cpp | 5 | ||||
-rw-r--r-- | jobs/syncjob.h | 1 | ||||
-rw-r--r-- | room.cpp | 23 | ||||
-rw-r--r-- | room.h | 5 |
12 files changed, 376 insertions, 45 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a345e06..9e3abce1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,11 +81,13 @@ set(libqmatrixclient_SRCS jobs/syncjob.cpp jobs/mediathumbnailjob.cpp jobs/logoutjob.cpp - ) +) + +aux_source_directory(jobs/generated libqmatrixclient_job_SRCS) set(example_SRCS examples/qmc-example.cpp) -add_library(qmatrixclient ${libqmatrixclient_SRCS}) +add_library(qmatrixclient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) set_property(TARGET qmatrixclient PROPERTY VERSION "0.0.0") set_property(TARGET qmatrixclient PROPERTY SOVERSION 0 ) diff --git a/connection.cpp b/connection.cpp index 2c9ee88a..513d9242 100644 --- a/connection.cpp +++ b/connection.cpp @@ -50,7 +50,11 @@ class Connection::Private Connection* q; ConnectionData* data; - QHash<QString, Room*> roomMap; + // A complex key below is a pair of room name and whether its + // state is Invited. The spec mandates to keep Invited room state + // separately so we should, e.g., keep objects for Invite and + // Leave state of the same room. + QHash<QPair<QString, bool>, Room*> roomMap; QHash<QString, User*> userMap; QString username; QString password; @@ -160,7 +164,7 @@ void Connection::sync(int timeout) d->data->setLastEvent(job->nextBatch()); for( auto&& roomData: job->takeRoomData() ) { - if ( auto* r = provideRoom(roomData.roomId) ) + if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) r->updateData(std::move(roomData)); } d->syncJob = nullptr; @@ -197,20 +201,12 @@ PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const JoinRoomJob* Connection::joinRoom(const QString& roomAlias) { - auto job = callApi<JoinRoomJob>(roomAlias); - connect( job, &BaseJob::success, [=] () { - if ( Room* r = provideRoom(job->roomId()) ) - emit joinedRoom(r); - }); - return job; + return callApi<JoinRoomJob>(roomAlias); } void Connection::leaveRoom(Room* room) { - auto job = callApi<LeaveRoomJob>(room->id()); - connect( job, &BaseJob::success, [=] () { - emit leftRoom(room); - }); + callApi<LeaveRoomJob>(room->id()); } RoomMessagesJob* Connection::getMessages(Room* room, const QString& from) const @@ -275,9 +271,18 @@ int Connection::millisToReconnect() const return d->syncJob ? d->syncJob->millisToRetry() : 0; } -QHash< QString, Room* > Connection::roomMap() const +QHash< QPair<QString, bool>, Room* > Connection::roomMap() const { - return d->roomMap; + // Copy-on-write-and-remove-elements is faster than copying elements one by one. + QHash< QPair<QString, bool>, Room* > roomMap = d->roomMap; + for (auto it = roomMap.begin(); it != roomMap.end(); ) + { + if (it.value()->joinState() == JoinState::Leave) + it = roomMap.erase(it); + else + ++it; + } + return roomMap; } const ConnectionData* Connection::connectionData() const @@ -285,36 +290,80 @@ const ConnectionData* Connection::connectionData() const return d->data; } -Room* Connection::provideRoom(const QString& id) +Room* Connection::provideRoom(const QString& id, JoinState joinState) { + // TODO: This whole function is a strong case for a RoomManager class. if (id.isEmpty()) { qCDebug(MAIN) << "Connection::provideRoom() with empty id, doing nothing"; return nullptr; } - if (d->roomMap.contains(id)) - return d->roomMap.value(id); - - // Not yet in the map, create a new one. - auto* room = createRoom(this, id); + // Room transitions: + // 1. none -> Invite: r=createRoom, emit invitedRoom(r,null) + // 2. none -> Join: r=createRoom, emit joinedRoom(r,null) + // 3. none -> Leave: r=createRoom, emit leftRoom(r,null) + // 4. inv=Invite -> Join: r=createRoom, emit joinedRoom(r,inv), delete Invite + // 4a. Leave, inv=Invite -> Join: change state, emit joinedRoom(r,inv), delete Invite + // 5. inv=Invite -> Leave: r=createRoom, emit leftRoom(r,inv), delete Invite + // 5a. r=Leave, inv=Invite -> Leave: emit leftRoom(r,inv), delete Invite + // 6. Join -> Leave: change state + // 7. r=Leave -> Invite: inv=createRoom, emit invitedRoom(inv,r) + // 8. Leave -> (changes to) Join + const auto roomKey = qMakePair(id, joinState == JoinState::Invite); + auto* room = d->roomMap.value(roomKey, nullptr); if (room) { - d->roomMap.insert( id, room ); + // Leave is a special case because in transition (5a) above + // joinState == room->joinState but we still have to preempt the Invite + // and emit a signal. For Invite and Join, there's no such problem. + if (room->joinState() == joinState && joinState != JoinState::Leave) + return room; + } + else + { + room = createRoom(this, id, joinState); + if (!room) + { + qCCritical(MAIN) << "Failed to create a room" << id; + return nullptr; + } + d->roomMap.insert(roomKey, room); + qCDebug(MAIN) << "Created Room" << id << ", invited:" << roomKey.second; emit newRoom(room); - } else { - qCritical() << "Failed to create a room!!!" << id; + } + if (joinState == JoinState::Invite) + { + // prev is either Leave or nullptr + auto* prev = d->roomMap.value({id, false}, nullptr); + emit invitedRoom(room, prev); + } + else + { + room->setJoinState(joinState); + // Preempt the Invite room (if any) with a room in Join/Leave state. + auto* prevInvite = d->roomMap.take({id, true}); + if (joinState == JoinState::Join) + emit joinedRoom(room, prevInvite); + else if (joinState == JoinState::Leave) + emit leftRoom(room, prevInvite); + if (prevInvite) + { + qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); + emit aboutToDeleteRoom(prevInvite); + delete prevInvite; + } } return room; } -std::function<Room*(Connection*, const QString&)> Connection::createRoom = - [](Connection* c, const QString& id) { return new Room(c, id); }; +Connection::room_factory_t Connection::createRoom = + [](Connection* c, const QString& id, JoinState joinState) + { return new Room(c, id, joinState); }; -std::function<User*(Connection*, const QString&)> Connection::createUser = +Connection::user_factory_t Connection::createUser = [](Connection* c, const QString& id) { return new User(id, c); }; - QByteArray Connection::generateTxnId() { return d->data->generateTxnId(); diff --git a/connection.h b/connection.h index 4b0413e3..0dcb8c3f 100644 --- a/connection.h +++ b/connection.h @@ -18,6 +18,8 @@ #pragma once +#include "joinstate.h" + #include <QtCore/QObject> #include <QtCore/QUrl> #include <QtCore/QSize> @@ -41,11 +43,16 @@ namespace QMatrixClient class Connection: public QObject { Q_OBJECT public: + using room_factory_t = + std::function<Room*(Connection*, const QString&, JoinState joinState)>; + using user_factory_t = + std::function<User*(Connection*, const QString&)>; + explicit Connection(const QUrl& server, QObject* parent = nullptr); Connection(); virtual ~Connection(); - QHash<QString, Room*> roomMap() const; + QHash<QPair<QString, bool>, Room*> roomMap() const; Q_INVOKABLE virtual void resolveServer(const QString& domain); Q_INVOKABLE virtual void connectToServer(const QString& user, @@ -102,7 +109,8 @@ namespace QMatrixClient static void setRoomType() { createRoom = - [](Connection* c, const QString& id) { return new T(c, id); }; + [](Connection* c, const QString& id, JoinState joinState) + { return new T(c, id, joinState); }; } template <typename T = User> @@ -120,8 +128,10 @@ namespace QMatrixClient void syncDone(); void newRoom(Room* room); - void joinedRoom(Room* room); - void leftRoom(Room* room); + void invitedRoom(Room* room, Room* prev); + void joinedRoom(Room* room, Room* prev); + void leftRoom(Room* room, Room* prev); + void aboutToDeleteRoom(Room* room); void loginError(QString error); void networkError(size_t nextAttempt, int inMilliseconds); @@ -144,13 +154,13 @@ namespace QMatrixClient * @return a pointer to a Room object with the specified id; nullptr * if roomId is empty if createRoom() failed to create a Room object. */ - Room* provideRoom(const QString& roomId); + Room* provideRoom(const QString& roomId, JoinState joinState); private: class Private; Private* d; - static std::function<Room*(Connection*, const QString&)> createRoom; - static std::function<User*(Connection*, const QString&)> createUser; + static room_factory_t createRoom; + static user_factory_t createUser; }; } // namespace QMatrixClient diff --git a/jobs/converters.h b/jobs/converters.h new file mode 100644 index 00000000..f9ab0269 --- /dev/null +++ b/jobs/converters.h @@ -0,0 +1,89 @@ +/****************************************************************************** +* Copyright (C) 2017 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 <QtCore/QJsonValue> +#include <QtCore/QJsonArray> +#include <QtCore/QDate> +#include <QtCore/QVariant> + +namespace QMatrixClient +{ + template <typename T> + inline QJsonValue toJson(T val) + { + return QJsonValue(val); + } + + template <typename T> + inline QJsonValue toJson(const QVector<T>& vals) + { + QJsonArray ar; + for (const auto& v: vals) + ar.push_back(toJson(v)); + return ar; + } + + inline QJsonValue toJson(const QStringList& strings) + { + return QJsonArray::fromStringList(strings); + } + + template <typename T> + inline T fromJson(const QJsonValue& jv) + { + return QVariant(jv).value<T>(); + } + + template <> + inline int fromJson<int>(const QJsonValue& jv) + { + return jv.toInt(); + } + + template <> + inline qint64 fromJson<qint64>(const QJsonValue& jv) + { + return static_cast<qint64>(jv.toDouble()); + } + + template <> + inline double fromJson<double>(const QJsonValue& jv) + { + return jv.toDouble(); + } + + template <> + inline QString fromJson<QString>(const QJsonValue& jv) + { + return jv.toString(); + } + + template <> + inline QDateTime fromJson<QDateTime>(const QJsonValue& jv) + { + return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); + } + + template <> + inline QDate fromJson<QDate>(const QJsonValue& jv) + { + return fromJson<QDateTime>(jv).date(); + } +} // namespace QMatrixClient diff --git a/jobs/generated/inviting.cpp b/jobs/generated/inviting.cpp new file mode 100644 index 00000000..e5e7f410 --- /dev/null +++ b/jobs/generated/inviting.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#include "inviting.h" + + +#include "../converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + + + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + + +InviteUserJob::InviteUserJob(const ConnectionData* connection, + QString roomId + , + QString user_id + ) + : BaseJob(connection, HttpVerb::Post, "InviteUserJob" + , basePath % "/rooms/" % roomId % "/invite" + , Query { } + , Data { + { "user_id", toJson(user_id) } + } + + ) +{ } + + + + diff --git a/jobs/generated/inviting.h b/jobs/generated/inviting.h new file mode 100644 index 00000000..af5a426d --- /dev/null +++ b/jobs/generated/inviting.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#pragma once + + +#include "../basejob.h" + + + +#include <QtCore/QString> + + + + +namespace QMatrixClient +{ + + + // Operations + + /** + + */ + class InviteUserJob : public BaseJob + { + public: + InviteUserJob(const ConnectionData* connection + + , + QString roomId + + , + QString user_id + ); + + }; + + +} // namespace QMatrixClient diff --git a/jobs/generated/kicking.cpp b/jobs/generated/kicking.cpp new file mode 100644 index 00000000..726f6fb0 --- /dev/null +++ b/jobs/generated/kicking.cpp @@ -0,0 +1,41 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#include "kicking.h" + + +#include "../converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + + + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + + +KickJob::KickJob(const ConnectionData* connection, + QString roomId + , + QString user_id + , + QString reason + ) + : BaseJob(connection, HttpVerb::Post, "KickJob" + , basePath % "/rooms/" % roomId % "/kick" + , Query { } + , Data { + { "user_id", toJson(user_id) }, + + { "reason", toJson(reason) } + } + + ) +{ } + + + + diff --git a/jobs/generated/kicking.h b/jobs/generated/kicking.h new file mode 100644 index 00000000..7b183b08 --- /dev/null +++ b/jobs/generated/kicking.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + + +#pragma once + + +#include "../basejob.h" + + + +#include <QtCore/QString> + + + + +namespace QMatrixClient +{ + + + // Operations + + /** + + */ + class KickJob : public BaseJob + { + public: + KickJob(const ConnectionData* connection + + , + QString roomId + + , + QString user_id + + , + QString reason + ); + + }; + + +} // namespace QMatrixClient diff --git a/jobs/syncjob.cpp b/jobs/syncjob.cpp index 29ddc2e6..38cfcb2a 100644 --- a/jobs/syncjob.cpp +++ b/jobs/syncjob.cpp @@ -99,15 +99,14 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, const QJsonObject& room_) : roomId(roomId_) , joinState(joinState_) - , state("state") + , state(joinState == JoinState::Invite ? "invite_state" : "state") , timeline("timeline") , ephemeral("ephemeral") , accountData("account_data") - , inviteState("invite_state") { switch (joinState) { case JoinState::Invite: - inviteState.fromJson(room_); + state.fromJson(room_); break; case JoinState::Join: state.fromJson(room_); diff --git a/jobs/syncjob.h b/jobs/syncjob.h index 07824e23..57a87c9f 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -51,7 +51,6 @@ namespace QMatrixClient Batch<RoomEvent> timeline; Batch<Event> ephemeral; Batch<Event> accountData; - Batch<Event> inviteState; bool timelineLimited; QString timelinePrevBatch; @@ -18,6 +18,9 @@ #include "room.h" +#include "jobs/generated/kicking.h" +#include "jobs/generated/inviting.h" + #include <array> #include <QtCore/QHash> @@ -48,9 +51,9 @@ class Room::Private typedef QMultiHash<QString, User*> members_map_t; typedef std::pair<rev_iter_t, rev_iter_t> rev_iter_pair_t; - Private(Connection* c, QString id_) + Private(Connection* c, QString id_, JoinState initialJoinState) : q(nullptr), connection(c), id(std::move(id_)) - , joinState(JoinState::Join), unreadMessages(false) + , joinState(initialJoinState), unreadMessages(false) , highlightCount(0), notificationCount(0), roomMessagesJob(nullptr) { } @@ -134,8 +137,8 @@ class Room::Private } }; -Room::Room(Connection* connection, QString id) - : QObject(connection), d(new Private(connection, id)) +Room::Room(Connection* connection, QString id, JoinState initialJoinState) + : QObject(connection), d(new Private(connection, id, initialJoinState)) { // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ @@ -194,6 +197,8 @@ void Room::setJoinState(JoinState state) if( state == oldState ) return; d->joinState = state; + qCDebug(MAIN) << "Room" << id() << "changed state: " + << int(oldState) << "->" << int(state); emit joinStateChanged(oldState, state); } @@ -601,11 +606,21 @@ void Room::Private::getPreviousContent(int limit) } } +void Room::inviteToRoom(const QString& memberId) const +{ + connection()->callApi<InviteUserJob>(id(), memberId); +} + void Room::leaveRoom() const { connection()->callApi<LeaveRoomJob>(id()); } +void Room::kickMember(const QString& memberId, const QString& reason) const +{ + connection()->callApi<KickJob>(id(), memberId, reason); +} + void Room::Private::dropDuplicateEvents(RoomEvents* events) const { // Collect all duplicate events at the end of the container @@ -77,7 +77,7 @@ namespace QMatrixClient using Timeline = std::deque<TimelineItem>; using rev_iter_t = Timeline::const_reverse_iterator; - Room(Connection* connection, QString id); + Room(Connection* connection, QString id, JoinState initialJoinState); virtual ~Room(); Connection* connection() const; @@ -154,7 +154,10 @@ namespace QMatrixClient void getPreviousContent(int limit = 10); + void inviteToRoom(const QString& memberId) const; void leaveRoom() const; + void kickMember(const QString& memberId, const QString& reason) const; + void userRenamed(User* user, QString oldName); /** Mark all messages in the room as read */ |