diff options
-rw-r--r-- | .appveyor.yml | 46 | ||||
-rw-r--r-- | connection.cpp | 44 | ||||
-rw-r--r-- | connection.h | 96 | ||||
-rw-r--r-- | jobs/requestdata.cpp | 4 | ||||
-rw-r--r-- | libqmatrixclient.pri | 7 | ||||
-rw-r--r-- | room.cpp | 53 | ||||
-rw-r--r-- | room.h | 3 | ||||
-rw-r--r-- | user.cpp | 25 | ||||
-rw-r--r-- | user.h | 35 |
9 files changed, 259 insertions, 54 deletions
diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..410ad12e --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,46 @@ +image: Visual Studio 2015 + +environment: + #DEPLOY_DIR: libqmatrixclient-%APPVEYOR_BUILD_VERSION% + matrix: + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + QTDIR: C:\Qt\5.9\msvc2017_64 + VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" + PLATFORM: + MAKETOOL: cmake + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + QTDIR: C:\Qt\5.9\msvc2017_64 + VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" + PLATFORM: + MAKETOOL: qmake + - QTDIR: C:\Qt\5.9\msvc2015 + VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\vcvarsall.bat" + PLATFORM: x86 + MAKETOOL: cmake + +init: +- call "%QTDIR%\bin\qtenv2.bat" +- set PATH=C:\Qt\Tools\QtCreator\bin;%PATH% +- call "%VCVARS%" %platform% +- cd /D "%APPVEYOR_BUILD_FOLDER%" + +before_build: +- git submodule update --init --recursive +- if %MAKETOOL% == cmake cmake -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_CXX_FLAGS="/EHsc /W3" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="%DEPLOY_DIR%" + +build_script: +- if %MAKETOOL% == cmake cmake --build build +- if %MAKETOOL% == qmake qmake && jom + +#after_build: +#- cmake --build build --target install +#- 7z a libqmatrixclient.zip "%DEPLOY_DIR%\" + +# Uncomment this to connect to the AppVeyor build worker +#on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + +test: off + +#artifacts: +#- path: libqmatrixclient.zip diff --git a/connection.cpp b/connection.cpp index e46d08aa..6a3cd957 100644 --- a/connection.cpp +++ b/connection.cpp @@ -24,6 +24,7 @@ #include "jobs/generated/login.h" #include "jobs/generated/logout.h" #include "jobs/generated/receipts.h" +#include "jobs/generated/leaving.h" #include "jobs/sendeventjob.h" #include "jobs/joinroomjob.h" #include "jobs/roommessagesjob.h" @@ -39,6 +40,7 @@ #include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> #include <QtCore/QRegularExpression> +#include <QtCore/QCoreApplication> using namespace QMatrixClient; @@ -60,7 +62,7 @@ class Connection::Private // Leave state of the same room. QHash<QPair<QString, bool>, Room*> roomMap; QVector<QString> roomIdsToForget; - QHash<QString, User*> userMap; + QMap<QString, User*> userMap; QString userId; SyncJob* syncJob = nullptr; @@ -273,6 +275,7 @@ void Connection::onSyncSuccess(SyncData &&data) { } if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) r->updateData(std::move(roomData)); + QCoreApplication::instance()->processEvents(); } } @@ -382,6 +385,31 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, return job; } +CreateRoomJob* Connection::createRoom(RoomVisibility visibility, + const QString& alias, const QString& name, const QString& topic, + const QVector<QString>& invites, const QString& presetName, + bool isDirect, bool guestsCanJoin, + const QVector<CreateRoomJob::StateEvent>& initialState, + const QVector<CreateRoomJob::Invite3pid>& invite3pids, + const QJsonObject creationContent) +{ + auto job = callApi<CreateRoomJob>( + visibility == PublishRoom ? "public" : "private", alias, name, + topic, invites, invite3pids, creationContent, initialState, + presetName, isDirect, guestsCanJoin); + connect(job, &BaseJob::success, this, [this,job] { + emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); + }); + return job; +} + +CreateRoomJob* Connection::createDirectChat(const QString& userId, + const QString& topic, const QString& name) +{ + return createRoom(UnpublishRoom, "", name, topic, {userId}, + "trusted_private_chat", true); +} + ForgetRoomJob* Connection::forgetRoom(const QString& id) { // To forget is hard :) First we should ensure the local user is not @@ -434,8 +462,9 @@ User* Connection::user(const QString& userId) { if( d->userMap.contains(userId) ) return d->userMap.value(userId); - auto* user = createUser(this, userId); + auto* user = userFactory(this, userId); d->userMap.insert(userId, user); + emit newUser(user); return user; } @@ -490,6 +519,11 @@ QHash< QPair<QString, bool>, Room* > Connection::roomMap() const return roomMap; } +QMap<QString, User*> Connection::users() const +{ + return d->userMap; +} + const ConnectionData* Connection::connectionData() const { return d->data.get(); @@ -512,7 +546,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) } else { - room = createRoom(this, id, joinState); + room = roomFactory(this, id, joinState); if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; @@ -547,11 +581,11 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) return room; } -Connection::room_factory_t Connection::createRoom = +Connection::room_factory_t Connection::roomFactory = [](Connection* c, const QString& id, JoinState joinState) { return new Room(c, id, joinState); }; -Connection::user_factory_t Connection::createUser = +Connection::user_factory_t Connection::userFactory = [](Connection* c, const QString& id) { return new User(id, c); }; QByteArray Connection::generateTxnId() diff --git a/connection.h b/connection.h index 79d7d658..3ec4fd9d 100644 --- a/connection.h +++ b/connection.h @@ -18,7 +18,7 @@ #pragma once -#include "jobs/generated/leaving.h" +#include "jobs/generated/create_room.h" #include "joinstate.h" #include <QtCore/QObject> @@ -39,6 +39,7 @@ namespace QMatrixClient class SyncData; class RoomMessagesJob; class PostReceiptJob; + class ForgetRoomJob; class MediaThumbnailJob; class JoinRoomJob; class UploadContentJob; @@ -51,6 +52,11 @@ namespace QMatrixClient /** Whether or not the rooms state should be cached locally * \sa loadState(), saveState() */ + Q_PROPERTY(User* localUser READ user CONSTANT) + Q_PROPERTY(QString localUserId READ userId CONSTANT) + Q_PROPERTY(QString deviceId READ deviceId CONSTANT) + Q_PROPERTY(QByteArray accessToken READ accessToken CONSTANT) + Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) public: using room_factory_t = @@ -58,36 +64,25 @@ namespace QMatrixClient using user_factory_t = std::function<User*(Connection*, const QString&)>; + enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob + explicit Connection(QObject* parent = nullptr); explicit Connection(const QUrl& server, QObject* parent = nullptr); virtual ~Connection(); QHash<QPair<QString, bool>, Room*> roomMap() const; - - /** Sends /forget to the server and also deletes room locally. - * This method is in Connection, not in Room, since it's a - * room lifecycle operation, and Connection is an acting room manager. - * It ensures that the local user is not a member of a room (running /leave, - * if necessary) then issues a /forget request and if that one doesn't fail - * deletion of the local Room object is ensured. - * \param id - the room id to forget - * \return - the ongoing /forget request to the server; note that the - * success() signal of this request is connected to deleteLater() - * of a respective room so by the moment this finishes, there might be no - * Room object anymore. - */ - ForgetRoomJob* forgetRoom(const QString& id); + QMap<QString, User*> users() const; // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES // (breaks back-compatibility) - Q_INVOKABLE QUrl homeserver() const; + QUrl homeserver() const; Q_INVOKABLE User* user(const QString& userId); - Q_INVOKABLE User* user(); - Q_INVOKABLE QString userId() const; - Q_INVOKABLE QString deviceId() const; + User* user(); + QString userId() const; + QString deviceId() const; /** @deprecated Use accessToken() instead. */ Q_INVOKABLE QString token() const; - Q_INVOKABLE QByteArray accessToken() const; + QByteArray accessToken() const; Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; @@ -145,7 +140,7 @@ namespace QMatrixClient template <typename T = Room> static void setRoomType() { - createRoom = + roomFactory = [](Connection* c, const QString& id, JoinState joinState) { return new T(c, id, joinState); }; } @@ -153,7 +148,7 @@ namespace QMatrixClient template <typename T = User> static void setUserType() { - createUser = + userFactory = [](Connection* c, const QString& id) { return new T(id, c); }; } @@ -186,19 +181,50 @@ namespace QMatrixClient int requestedHeight) const; // QIODevice* should already be open - virtual UploadContentJob* uploadContent(QIODevice* contentSource, + UploadContentJob* uploadContent(QIODevice* contentSource, const QString& filename = {}, const QString& contentType = {}) const; - virtual UploadContentJob* uploadFile(const QString& fileName, - const QString& contentType = {}); - virtual GetContentJob* getContent(const QString& mediaId) const; + UploadContentJob* uploadFile(const QString& fileName, + const QString& contentType = {}); + GetContentJob* getContent(const QString& mediaId) const; GetContentJob* getContent(const QUrl& url) const; // If localFilename is empty, a temporary file will be created - virtual DownloadFileJob* downloadFile(const QUrl& url, + DownloadFileJob* downloadFile(const QUrl& url, const QString& localFilename = {}) const; + /** + * \brief Create a room (generic method) + * This method allows to customize room entirely to your liking, + * providing all the attributes the original CS API provides. + */ + CreateRoomJob* createRoom(RoomVisibility visibility, + const QString& alias, const QString& name, const QString& topic, + const QVector<QString>& invites, const QString& presetName = {}, bool isDirect = false, + bool guestsCanJoin = false, + const QVector<CreateRoomJob::StateEvent>& initialState = {}, + const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, + const QJsonObject creationContent = {}); + + /** Create a direct chat with a single user, optional name and topic */ + CreateRoomJob* createDirectChat(const QString& userId, + const QString& topic = {}, const QString& name = {}); + virtual JoinRoomJob* joinRoom(const QString& roomAlias); + /** Sends /forget to the server and also deletes room locally. + * This method is in Connection, not in Room, since it's a + * room lifecycle operation, and Connection is an acting room manager. + * It ensures that the local user is not a member of a room (running /leave, + * if necessary) then issues a /forget request and if that one doesn't fail + * deletion of the local Room object is ensured. + * \param id - the room id to forget + * \return - the ongoing /forget request to the server; note that the + * success() signal of this request is connected to deleteLater() + * of a respective room so by the moment this finishes, there might be no + * Room object anymore. + */ + ForgetRoomJob* forgetRoom(const QString& id); + // Old API that will be abolished any time soon. DO NOT USE. /** @deprecated Use callApi<PostMessageJob>() or Room::postMessage() instead */ @@ -237,6 +263,8 @@ namespace QMatrixClient void syncDone(); void syncError(QString error); + void newUser(User* user); + /** * \group Signals emitted on room transitions * @@ -295,6 +323,13 @@ namespace QMatrixClient /** The room object is about to be deleted */ void aboutToDeleteRoom(Room* room); + /** The room has just been created by createRoom or createDirectChat + * This signal is not emitted in usual room state transitions, + * only as an outcome of room creation operations invoked by + * the client. + */ + void createdRoom(Room* room); + void cacheStateChanged(); protected: @@ -310,7 +345,7 @@ namespace QMatrixClient * the server; in particular, does not automatically create rooms * on the server. * @return a pointer to a Room object with the specified id; nullptr - * if roomId is empty if createRoom() failed to create a Room object. + * if roomId is empty if roomFactory() failed to create a Room object. */ Room* provideRoom(const QString& roomId, JoinState joinState); @@ -340,7 +375,8 @@ namespace QMatrixClient const QString& initialDeviceName, const QString& deviceId = {}); - static room_factory_t createRoom; - static user_factory_t createUser; + static room_factory_t roomFactory; + static user_factory_t userFactory; }; } // namespace QMatrixClient +Q_DECLARE_METATYPE(QMatrixClient::Connection*) diff --git a/jobs/requestdata.cpp b/jobs/requestdata.cpp index f5516c5f..5cb62221 100644 --- a/jobs/requestdata.cpp +++ b/jobs/requestdata.cpp @@ -8,7 +8,7 @@ using namespace QMatrixClient; -std::unique_ptr<QIODevice> fromData(const QByteArray& data) +auto fromData(const QByteArray& data) { auto source = std::make_unique<QBuffer>(); source->open(QIODevice::WriteOnly); @@ -18,7 +18,7 @@ std::unique_ptr<QIODevice> fromData(const QByteArray& data) } template <typename JsonDataT> -inline std::unique_ptr<QIODevice> fromJson(const JsonDataT& jdata) +inline auto fromJson(const JsonDataT& jdata) { return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact)); } diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 9e4cb279..72637caf 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -1,6 +1,11 @@ QT += network CONFIG += c++14 warn_on rtti_off -QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter + +win32-msvc* { + QMAKE_CXXFLAGS_WARN_ON += -wd4100 +} else { + QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter +} INCLUDEPATH += $$PWD @@ -99,7 +99,7 @@ class Room::Private struct FileTransferPrivateInfo { -#if defined(_MSC_VER) && _MSC_VER < 1910 +#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) FileTransferPrivateInfo() = default; FileTransferPrivateInfo(BaseJob* j, QString fileName) : job(j), localFileInfo(fileName) @@ -119,6 +119,9 @@ class Room::Private if (p == 0) p = -1; } + if (p != -1) + qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t + << "=" << llround(double(p) / t * 100) << "%"; progress = p; total = t; } }; @@ -543,6 +546,25 @@ void Room::resetHighlightCount() emit highlightCountChanged(this); } +QString Room::fileNameToDownload(const QString& eventId) +{ + auto evtIt = findInTimeline(eventId); + if (evtIt != timelineEdge() && + evtIt->event()->type() == EventType::RoomMessage) + { + auto* event = static_cast<const RoomMessageEvent*>(evtIt->event()); + if (event->hasFileContent()) + { + auto* fileInfo = event->content()->fileInfo(); + return !fileInfo->originalName.isEmpty() ? fileInfo->originalName : + !event->plainBody().isEmpty() ? event->plainBody() : + QString(); + } + } + qWarning() << "No files to download in event" << eventId; + return {}; +} + FileTransferInfo Room::fileTransferInfo(const QString& id) const { auto infoIt = d->fileTransfers.find(id); @@ -561,7 +583,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const total = INT_MAX; } -#if defined(_MSC_VER) && _MSC_VER < 1910 +#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) // A workaround for MSVC 2015 that fails with "error C2440: 'return': // cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" FileTransferInfo fti; @@ -790,8 +812,8 @@ QString Room::roomMembername(const User* u) const // << "is not a member of the room" << id(); // } - // In case of more than one namesake, disambiguate with user id. - return username % " (" % u->id() % ")"; + // In case of more than one namesake, use the full name to disambiguate + return u->fullName(); } QString Room::roomMembername(const QString& userId) const @@ -862,6 +884,17 @@ void Room::postMessage(const RoomMessageEvent& event) connection()->callApi<SendEventJob>(id(), event); } +void Room::setName(const QString& newName) +{ + connection()->callApi<SetRoomStateJob>(id(), RoomNameEvent(newName)); +} + +void Room::setCanonicalAlias(const QString& newAlias) +{ + connection()->callApi<SetRoomStateJob>(id(), + RoomCanonicalAliasEvent(newAlias)); +} + void Room::setTopic(const QString& newTopic) { RoomTopicEvent evt(newTopic); @@ -970,8 +1003,16 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) auto fileName = !localFilename.isEmpty() ? localFilename.toLocalFile() : !fileInfo->originalName.isEmpty() ? (safeTempPrefix + fileInfo->originalName) : - !event->plainBody().isEmpty() ? - (safeTempPrefix + event->plainBody()) : QString(); + !event->plainBody().isEmpty() ? (safeTempPrefix + event->plainBody()) : + (safeTempPrefix + fileInfo->mimeType.preferredSuffix()); + if (QSysInfo::productType() == "windows") + { + const auto& suffixes = fileInfo->mimeType.suffixes(); + if (!suffixes.isEmpty() && + std::none_of(suffixes.begin(), suffixes.end(), + [fileName] (const QString& s) { return fileName.endsWith(s); })) + fileName += '.' + fileInfo->mimeType.preferredSuffix(); + } auto job = connection()->downloadFile(fileInfo->url, fileName); if (isJobRunning(job)) { @@ -215,6 +215,7 @@ namespace QMatrixClient Q_INVOKABLE int highlightCount() const; Q_INVOKABLE void resetHighlightCount(); + Q_INVOKABLE QString fileNameToDownload(const QString& eventId); Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; /** Pretty-prints plain text into HTML @@ -235,6 +236,8 @@ namespace QMatrixClient /** @deprecated If you have a custom event type, construct the event * and pass it as a whole to postMessage() */ void postMessage(const QString& type, const QString& plainText); + void setName(const QString& newName); + void setCanonicalAlias(const QString& newAlias); void setTopic(const QString& newTopic); void getPreviousContent(int limit = 10); @@ -28,6 +28,9 @@ #include <QtCore/QTimer> #include <QtCore/QRegularExpression> #include <QtCore/QPointer> +#include <QtCore/QStringBuilder> + +#include <functional> using namespace QMatrixClient; @@ -65,6 +68,15 @@ QString User::id() const return d->userId; } +bool User::isGuest() const +{ + Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); + auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), + [] (QChar c) { return c.isDigit(); }); + Q_ASSERT(it != d->userId.end()); + return *it == ':'; +} + QString User::name() const { return d->name; @@ -121,12 +133,17 @@ void User::Private::setAvatar(UploadContentJob* job, User* q) QString User::displayname() const { - if( !d->name.isEmpty() ) - return d->name; - return d->userId; + return d->name.isEmpty() ? d->userId : d->name; } -QString User::bridged() const { +QString User::fullName() const +{ + return d->name.isEmpty() ? d->userId : + d->name % " (" % d->userId % ')'; +} + +QString User::bridged() const +{ return d->bridged; } @@ -30,8 +30,10 @@ namespace QMatrixClient { Q_OBJECT Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(bool isGuest READ isGuest CONSTANT) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) + Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) @@ -39,26 +41,47 @@ namespace QMatrixClient User(QString userId, Connection* connection); ~User() override; - /** - * Returns the id of the user + /** Get unique stable user id + * User id is generated by the server and is not changed ever. */ QString id() const; - /** - * Returns the name chosen by the user + /** Get the name chosen by the user + * This may be empty if the user didn't choose the name or cleared + * it. + * \sa displayName */ QString name() const; - /** - * Returns the name that should be used to display the user. + /** 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 */ QString displayname() 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. + * \sa displayName, Room::roomMembername + */ + QString fullName() const; + /** * Returns the name of bridge the user is connected from or empty. */ QString bridged() const; + /** Whether the user is a guest + * As of now, the function relies on the convention used in Synapse + * that guests and only guests have all-numeric IDs. This may or + * may not work with non-Synapse servers. + */ + bool isGuest() const; + const Avatar& avatarObject() const; Q_INVOKABLE QImage avatar(int dimension); Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight); |