aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp37
-rw-r--r--lib/connection.h16
-rw-r--r--lib/events/roommessageevent.cpp64
-rw-r--r--lib/events/roommessageevent.h15
-rw-r--r--lib/room.cpp113
-rw-r--r--lib/room.h4
-rw-r--r--lib/util.h8
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) {
diff --git a/lib/room.h b/lib/room.h
index 197926e7..a9341bd2 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -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);
diff --git a/lib/util.h b/lib/util.h
index 596872e2..f7f646da 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -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