diff options
-rw-r--r-- | lib/connection.cpp | 37 | ||||
-rw-r--r-- | lib/connection.h | 16 | ||||
-rw-r--r-- | lib/events/roommessageevent.cpp | 64 | ||||
-rw-r--r-- | lib/events/roommessageevent.h | 15 | ||||
-rw-r--r-- | lib/room.cpp | 113 | ||||
-rw-r--r-- | lib/room.h | 4 | ||||
-rw-r--r-- | lib/util.h | 8 |
7 files changed, 189 insertions, 68 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index 4c0fe6b8..998282d3 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -83,6 +83,8 @@ class Connection::Private // separately so we should, e.g., keep objects for Invite and // Leave state of the same room. QHash<QPair<QString, bool>, Room*> roomMap; + // Mapping from aliases to room ids, as per the last sync + QHash<QString, QString> roomAliasMap; QVector<QString> roomIdsToForget; QVector<Room*> firstTimeRooms; QVector<QString> pendingStateRoomIds; @@ -802,6 +804,41 @@ Room* Connection::room(const QString& roomId, JoinStates states) const return nullptr; } +Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const +{ + const auto id = d->roomAliasMap.value(roomAlias); + if (!id.isEmpty()) + return room(id, states); + qCWarning(MAIN) << "Room for alias" << roomAlias + << "is not found under account" << userId(); + return nullptr; +} + +void Connection::updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases) +{ + for (const auto& a: previousRoomAliases) + if (d->roomAliasMap.remove(a) == 0) + qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; + + for (const auto& a: roomAliases) + { + auto& mappedId = d->roomAliasMap[a]; + if (!mappedId.isEmpty()) + { + if (mappedId == roomId) + qCDebug(MAIN) << "Alias" << a << "is already mapped to room" + << roomId; + else + qCWarning(MAIN) << "Alias" << a + << "will be force-remapped from room" + << mappedId << "to" << roomId; + } + mappedId = roomId; + } +} + Room* Connection::invitation(const QString& roomId) const { return d->roomMap.value({roomId, true}, nullptr); diff --git a/lib/connection.h b/lib/connection.h index 1a81c29e..b22d63da 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -231,8 +231,8 @@ namespace QMatrixClient */ void addToIgnoredUsers(const User* user); - /** Remove the user from the ignore list - * Similar to adding, the change signal is emitted synchronously. + /** Remove the user from the ignore list */ + /** Similar to adding, the change signal is emitted synchronously. * * \sa ignoredUsersListChanged */ @@ -242,8 +242,18 @@ namespace QMatrixClient QMap<QString, User*> users() const; QUrl homeserver() const; + /** Find a room by its id and a mask of applicable states */ Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite|JoinState::Join) const; + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Find a room by its alias and a mask of applicable states */ + Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Update the internal map of room aliases to IDs */ + /// This is used for internal bookkeeping of rooms. Do NOT use + /// it to try change aliases, use Room::setAliases instead + void updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; Q_INVOKABLE User* user(const QString& userId); const User* user() const; diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index c3007fa0..8f4e0ebc 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -30,12 +30,29 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; +static const auto RelatesToKey = "m.relates_to"_ls; +static const auto MsgTypeKey = "msgtype"_ls; +static const auto BodyKey = "body"_ls; +static const auto FormattedBodyKey = "formatted_body"_ls; + +static const auto TextTypeKey = "m.text"; +static const auto NoticeTypeKey = "m.notice"; + +static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); + template <typename ContentT> TypedBase* make(const QJsonObject& json) { return new ContentT(json); } +template <> +TypedBase* make<TextContent>(const QJsonObject& json) +{ + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + ? new TextContent(json) : nullptr; +} + struct MsgTypeDesc { QString matrixType; @@ -44,9 +61,9 @@ struct MsgTypeDesc }; const std::vector<MsgTypeDesc> msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> } + { { TextTypeKey, MsgType::Text, make<TextContent> } , { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> } - , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> } + , { NoticeTypeKey, MsgType::Notice, make<TextContent> } , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> } , { QStringLiteral("m.file"), MsgType::File, make<FileContent> } , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> } @@ -78,14 +95,18 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && + json.contains(RelatesToKey)) + { + json.remove(RelatesToKey); + qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); return json; } -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; - RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), @@ -141,13 +162,17 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) { auto msgtype = content[MsgTypeKey].toString(); + bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) + { _content.reset(mt.maker(content)); + msgTypeFound = true; + } - if (!_content) + if (!msgTypeFound) { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } @@ -179,14 +204,13 @@ QMimeType RoomMessageEvent::mimeType() const static const auto PlainTextMimeType = QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; - ; } bool RoomMessageEvent::hasTextContent() const { - return content() && + return !content() || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes + msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -218,10 +242,12 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); } -TextContent::TextContent(const QString& text, const QString& contentType) +TextContent::TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) + , relatesTo(std::move(relatesTo)) { - if (contentType == "org.matrix.custom.html") + if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } @@ -233,16 +259,20 @@ TextContent::TextContent(const QJsonObject& json) // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == "org.matrix.custom.html") + if (json["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json["formatted_body"_ls].toString(); + body = json[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; body = json[BodyKey].toString(); } + const auto replyJson = json[RelatesToKey].toObject() + .value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + relatesTo = replyTo(fromJson<QString>(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const @@ -250,10 +280,12 @@ void TextContent::fillJson(QJsonObject* json) const Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), - QStringLiteral("org.matrix.custom.html")); + json->insert(QStringLiteral("format"), HtmlContentTypeId); json->insert(QStringLiteral("formatted_body"), body); } + if (!relatesTo.omitted()) + json->insert(QStringLiteral("m.relates_to"), + QJsonObject { { relatesTo->type, relatesTo->eventId } }); } LocationContent::LocationContent(const QString& geoUri, diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index d5b570f5..c2e075eb 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -92,6 +92,17 @@ namespace QMatrixClient { // Additional event content types + struct RelatesTo + { + static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + QString type; // The only supported relation so far + QString eventId; + }; + inline RelatesTo replyTo(QString eventId) + { + return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + } + /** * Rich text content for m.text, m.emote, m.notice * @@ -101,13 +112,15 @@ namespace QMatrixClient class TextContent: public TypedBase { public: - TextContent(const QString& text, const QString& contentType); + TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; + Omittable<RelatesTo> relatesTo; protected: void fillJson(QJsonObject* json) const override; diff --git a/lib/room.cpp b/lib/room.cpp index c6376a26..b0be288b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -72,14 +72,6 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 and older GCC's that don't handle initializer -// lists right (MSVC 2015, notably, fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'") -#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ - (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) -# define WORKAROUND_EXTENDED_INITIALIZER_LIST -#endif - class Room::Private { public: @@ -209,6 +201,28 @@ class Room::Private is<RoomMessageEvent>(*ti); } + template <typename EventArrayT> + Changes updateStateFrom(EventArrayT&& events) + { + Changes changes = NoChange; + if (!events.empty()) + { + QElapsedTimer et; et.start(); + for (auto&& eptr: events) + { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + // Update baseState afterwards to make sure that the old state + // is valid and usable inside processStateEvent + changes |= q->processStateEvent(evt); + baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); + } + if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" + << events.size() << "event(s)," << et; + } + return changes; + } Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); @@ -684,13 +698,7 @@ void Room::Private::getAllMembers() auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; connect( allMembersJob, &BaseJob::success, q, [=] { Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); - Changes roomChanges = NoChange; - for (auto&& e: allMembersJob->chunk()) - { - const auto& evt = *e; - baseState[{evt.matrixType(),evt.stateKey()}] = move(e); - roomChanges |= q->processStateEvent(evt); - } + auto roomChanges = updateStateFrom(allMembersJob->chunk()); // Replay member events that arrived after the point for which // the full members list was requested. if (!timeline.empty() ) @@ -1049,7 +1057,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const total = INT_MAX; } -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST +#ifdef BROKEN_INITIALIZER_LISTS FileTransferInfo fti; fti.status = infoIt->status; fti.progress = int(progress); @@ -1296,21 +1304,8 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) for (auto&& event: data.accountData) roomChanges |= processAccountDataEvent(move(event)); - if (!data.state.empty()) - { - et.restart(); - for (auto&& eptr: data.state) - { - const auto& evt = *eptr; - Q_ASSERT(evt.isStateEvent()); - d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); - roomChanges |= processStateEvent(evt); - } + roomChanges |= d->updateStateFrom(data.state); - if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; - } if (!data.timeline.empty()) { et.restart(); @@ -1366,8 +1361,6 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); - // FIXME: This sometimes causes a bad read: - // https://travis-ci.org/QMatrixClient/libqmatrixclient/jobs/492156899#L2596 unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); return pEvent; @@ -1522,7 +1515,7 @@ QString Room::postPlainText(const QString& plainText) } QString Room::postHtmlMessage(const QString& plainText, const QString& html, - MessageEventType type) + MessageEventType type) { return d->sendEvent<RoomMessageEvent>(plainText, type, new EventContent::TextContent(html, QStringLiteral("text/html"))); @@ -1530,7 +1523,7 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html, QString Room::postHtmlText(const QString& plainText, const QString& html) { - return postHtmlMessage(plainText, html, MessageEventType::Text); + return postHtmlMessage(plainText, html); } QString Room::postFile(const QString& plainText, const QUrl& localPath, @@ -1614,6 +1607,11 @@ void Room::setCanonicalAlias(const QString& newAlias) d->requestSetState(RoomCanonicalAliasEvent(newAlias)); } +void Room::setAliases(const QStringList& aliases) +{ + d->requestSetState(RoomAliasesEvent(aliases)); +} + void Room::setTopic(const QString& newTopic) { d->requestSetState(RoomTopicEvent(newTopic)); @@ -1881,22 +1879,29 @@ RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { auto originalJson = target.originalJsonObject(); - static const QStringList keepKeys = - { EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), - QStringLiteral("prev_content"), ContentKey, - QStringLiteral("origin_server_ts") }; + static const QStringList keepKeys { + EventIdKey, TypeKey, QStringLiteral("room_id"), + QStringLiteral("sender"), QStringLiteral("state_key"), + QStringLiteral("prev_content"), ContentKey, + QStringLiteral("hashes"), QStringLiteral("signatures"), + QStringLiteral("depth"), QStringLiteral("prev_events"), + QStringLiteral("prev_state"), QStringLiteral("auth_events"), + QStringLiteral("origin"), QStringLiteral("origin_server_ts"), + QStringLiteral("membership") + }; std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } - , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } + , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomPowerLevels::typeId(), // { QStringLiteral("ban"), QStringLiteral("events"), // QStringLiteral("events_default"), QStringLiteral("kick"), // QStringLiteral("redact"), QStringLiteral("state_default"), // QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } } + , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } +// , { RoomHistoryVisibility::typeId(), +// { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { @@ -2050,15 +2055,21 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) it = nextPending + 1; auto* nextPendingEvt = nextPending->get(); - emit q->pendingEventAboutToMerge(nextPendingEvt, - int(nextPendingPair.second - unsyncedEvents.begin())); + const auto pendingEvtIdx = + int(nextPendingPair.second - unsyncedEvents.begin()); + emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); qDebug(EVENTS) << "Merging pending event from transaction" << nextPendingEvt->transactionId() << "into" << nextPendingEvt->id(); auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); if (transfer.status != FileTransferInfo::None) fileTransfers.insert(nextPendingEvt->id(), transfer); - unsyncedEvents.erase(nextPendingPair.second); + // After emitting pendingEventAboutToMerge() above we cannot rely + // on the previously obtained nextPendingPair.second staying valid + // because a signal handler may send another message, thereby altering + // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at + // its back so we can rely on the index staying valid at least. + unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) { totalInserted += insertedSize; @@ -2148,8 +2159,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!e.isStateEvent()) return Change::NoChange; - d->currentState[{e.matrixType(),e.stateKey()}] = - static_cast<const StateEventBase*>(&e); + const auto* oldStateEvent = std::exchange( + d->currentState[{e.matrixType(),e.stateKey()}], + static_cast<const StateEventBase*>(&e)); + Q_ASSERT(!oldStateEvent || + (oldStateEvent->matrixType() == e.matrixType() && + oldStateEvent->stateKey() == e.stateKey())); if (!is<RoomMemberEvent>(e)) qCDebug(EVENTS) << "Room state event:" << e; @@ -2157,7 +2172,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) , [] (const RoomNameEvent&) { return NameChange; } - , [] (const RoomAliasesEvent&) { + , [this,oldStateEvent] (const RoomAliasesEvent& ae) { + const auto previousAliases = oldStateEvent + ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases() + : QStringList(); + connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); return OtherChange; } , [this] (const RoomCanonicalAliasEvent& evt) { @@ -385,7 +385,8 @@ namespace QMatrixClient QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); QString postHtmlMessage(const QString& plainText, - const QString& html, MessageEventType type); + const QString& html, + MessageEventType type = MessageEventType::Text); QString postHtmlText(const QString& plainText, const QString& html); QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); @@ -402,6 +403,7 @@ namespace QMatrixClient void discardMessage(const QString& txnId); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setAliases(const QStringList& aliases); void setTopic(const QString& newTopic); void getPreviousContent(int limit = 10); @@ -52,6 +52,14 @@ template <typename T> static void qAsConst(const T &&) Q_DECL_EQ_DELETE; #endif +// MSVC 2015 and older GCC's don't handle initialisation from initializer lists +// right in the absense of a constructor; MSVC 2015, notably, fails with +// "error C2440: 'return': cannot convert from 'initializer list' to '<type>'" +#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ + (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) +# define BROKEN_INITIALIZER_LISTS +#endif + namespace QMatrixClient { // The below enables pretty-printing of enums in logs |