diff --git a/src/Cache.cpp b/src/Cache.cpp
index d435dc56..91cde9e7 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -33,11 +33,12 @@
#include "Cache_p.h"
#include "EventAccessors.h"
#include "Logging.h"
+#include "Olm.h"
#include "Utils.h"
//! Should be changed when a breaking change occurs in the cache format.
//! This will reset client's data.
-static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.05.01");
+static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.07.05");
static const std::string SECRET("secret");
static lmdb::val NEXT_BATCH_KEY("next_batch");
@@ -46,8 +47,9 @@ static lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
constexpr size_t MAX_RESTORED_MESSAGES = 30'000;
-constexpr auto DB_SIZE = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB
-constexpr auto MAX_DBS = 8092UL;
+constexpr auto DB_SIZE = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB
+constexpr auto MAX_DBS = 8092UL;
+constexpr auto BATCH_SIZE = 100;
//! Cache databases and their format.
//!
@@ -63,7 +65,6 @@ constexpr auto SYNC_STATE_DB("sync_state");
//! Read receipts per room/event.
constexpr auto READ_RECEIPTS_DB("read_receipts");
constexpr auto NOTIFICATIONS_DB("sent_notifications");
-//! TODO: delete pending_receipts database on old cache versions
//! Encryption related databases.
@@ -93,18 +94,31 @@ namespace {
std::unique_ptr<Cache> instance_ = nullptr;
}
-int
-numeric_key_comparison(const MDB_val *a, const MDB_val *b)
+static bool
+isHiddenEvent(mtx::events::collections::TimelineEvents e, const std::string &room_id)
{
- auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size));
- auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size));
+ using namespace mtx::events;
+ if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
+ MegolmSessionIndex index;
+ index.room_id = room_id;
+ index.session_id = encryptedEvent->content.session_id;
+ index.sender_key = encryptedEvent->content.sender_key;
- if (lhs < rhs)
- return 1;
- else if (lhs == rhs)
- return 0;
+ auto result = olm::decryptEvent(index, *encryptedEvent);
+ if (!result.error)
+ e = result.event.value();
+ }
- return -1;
+ static constexpr std::initializer_list<EventType> hiddenEvents = {
+ EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
+
+ return std::visit(
+ [](const auto &ev) {
+ return std::any_of(hiddenEvents.begin(),
+ hiddenEvents.end(),
+ [ev](EventType type) { return type == ev.type; });
+ },
+ e);
}
Cache::Cache(const QString &userId, QObject *parent)
@@ -154,7 +168,10 @@ Cache::setup()
}
try {
- env_.open(statePath.toStdString().c_str());
+ // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
+ // it can really mess up our database, so we shouldn't. For now, hopefully
+ // NOMETASYNC is fast enough.
+ env_.open(statePath.toStdString().c_str(), MDB_NOMETASYNC);
} catch (const lmdb::error &e) {
if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
throw std::runtime_error("LMDB initialization failed" +
@@ -700,8 +717,77 @@ Cache::runMigrations()
nhlog::db()->info("Successfully deleted pending receipts database.");
return true;
}},
+ {"2020.07.05",
+ [this]() {
+ try {
+ auto txn = lmdb::txn::begin(env_, nullptr);
+ auto room_ids = getRoomIds(txn);
+
+ for (const auto &room_id : room_ids) {
+ try {
+ auto messagesDb = lmdb::dbi::open(
+ txn, std::string(room_id + "/messages").c_str());
+
+ // keep some old messages and batch token
+ {
+ auto roomsCursor =
+ lmdb::cursor::open(txn, messagesDb);
+ lmdb::val ts, stored_message;
+ bool start = true;
+ mtx::responses::Timeline oldMessages;
+ while (roomsCursor.get(ts,
+ stored_message,
+ start ? MDB_FIRST
+ : MDB_NEXT)) {
+ start = false;
+
+ auto j = json::parse(std::string_view(
+ stored_message.data(),
+ stored_message.size()));
+
+ if (oldMessages.prev_batch.empty())
+ oldMessages.prev_batch =
+ j["token"].get<std::string>();
+ else if (j["token"] !=
+ oldMessages.prev_batch)
+ break;
+
+ mtx::events::collections::TimelineEvent
+ te;
+ mtx::events::collections::from_json(
+ j["event"], te);
+ oldMessages.events.push_back(te.data);
+ }
+ // messages were stored in reverse order, so we
+ // need to reverse them
+ std::reverse(oldMessages.events.begin(),
+ oldMessages.events.end());
+ // save messages using the new method
+ saveTimelineMessages(txn, room_id, oldMessages);
+ }
+
+ // delete old messages db
+ lmdb::dbi_drop(txn, messagesDb, true);
+ } catch (std::exception &e) {
+ nhlog::db()->error(
+ "While migrating messages from {}, ignoring error {}",
+ room_id,
+ e.what());
+ }
+ }
+ txn.commit();
+ } catch (const lmdb::error &) {
+ nhlog::db()->critical(
+ "Failed to delete messages database in migration!");
+ return false;
+ }
+
+ nhlog::db()->info("Successfully deleted pending receipts database.");
+ return true;
+ }},
};
+ nhlog::db()->info("Running migrations, this may take a while!");
for (const auto &[target_version, migration] : migrations) {
if (target_version > stored_version)
if (!migration()) {
@@ -709,6 +795,7 @@ Cache::runMigrations()
return false;
}
}
+ nhlog::db()->info("Migrations finished.");
setCurrentFormat();
return true;
@@ -771,8 +858,9 @@ Cache::readReceipts(const QString &event_id, const QString &room_id)
txn.commit();
if (res) {
- auto json_response = json::parse(std::string(value.data(), value.size()));
- auto values = json_response.get<std::map<std::string, uint64_t>>();
+ auto json_response =
+ json::parse(std::string_view(value.data(), value.size()));
+ auto values = json_response.get<std::map<std::string, uint64_t>>();
for (const auto &v : values)
// timestamp, user_id
@@ -810,8 +898,8 @@ Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Recei
// If an entry for the event id already exists, we would
// merge the existing receipts with the new ones.
if (exists) {
- auto json_value =
- json::parse(std::string(prev_value.data(), prev_value.size()));
+ auto json_value = json::parse(
+ std::string_view(prev_value.data(), prev_value.size()));
// Retrieve the saved receipts.
saved_receipts = json_value.get<std::map<std::string, uint64_t>>();
@@ -930,7 +1018,7 @@ Cache::saveState(const mtx::responses::Sync &res)
if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room.first), data)) {
try {
RoomInfo tmp =
- json::parse(std::string(data.data(), data.size()));
+ json::parse(std::string_view(data.data(), data.size()));
updatedInfo.tags = tmp.tags;
} catch (const json::exception &e) {
nhlog::db()->warn(
@@ -1122,7 +1210,7 @@ Cache::singleRoomInfo(const std::string &room_id)
// Check if the room is joined.
if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room_id), data)) {
try {
- RoomInfo tmp = json::parse(std::string(data.data(), data.size()));
+ RoomInfo tmp = json::parse(std::string_view(data.data(), data.size()));
tmp.member_count = getMembersDb(txn, room_id).size(txn);
tmp.join_rule = getRoomJoinRule(txn, statesdb);
tmp.guest_access = getRoomGuestAccess(txn, statesdb);
@@ -1157,7 +1245,8 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
// Check if the room is joined.
if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room), data)) {
try {
- RoomInfo tmp = json::parse(std::string(data.data(), data.size()));
+ RoomInfo tmp =
+ json::parse(std::string_view(data.data(), data.size()));
tmp.member_count = getMembersDb(txn, room).size(txn);
tmp.join_rule = getRoomJoinRule(txn, statesdb);
tmp.guest_access = getRoomGuestAccess(txn, statesdb);
@@ -1173,7 +1262,7 @@ Cache::getRoomInfo(const std::vector<std::string> &rooms)
if (lmdb::dbi_get(txn, invitesDb_, lmdb::val(room), data)) {
try {
RoomInfo tmp =
- json::parse(std::string(data.data(), data.size()));
+ json::parse(std::string_view(data.data(), data.size()));
tmp.member_count = getInviteMembersDb(txn, room).size(txn);
room_info.emplace(QString::fromStdString(room),
@@ -1232,38 +1321,147 @@ Cache::getTimelineMentions()
return notifs;
}
-mtx::responses::Timeline
-Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id)
+std::string
+Cache::previousBatchToken(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr);
+ auto orderDb = getEventOrderDb(txn, room_id);
+
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ lmdb::val indexVal, val;
+ if (!cursor.get(indexVal, val, MDB_FIRST)) {
+ return "";
+ }
+
+ auto j = json::parse(std::string_view(val.data(), val.size()));
+
+ return j.value("prev_batch", "");
+}
+
+Cache::Messages
+Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t index, bool forward)
{
// TODO(nico): Limit the messages returned by this maybe?
- auto db = getMessagesDb(txn, room_id);
+ auto orderDb = getOrderToMessageDb(txn, room_id);
+ auto eventsDb = getEventsDb(txn, room_id);
- mtx::responses::Timeline timeline;
- std::string timestamp, msg;
+ Messages messages{};
- auto cursor = lmdb::cursor::open(txn, db);
+ lmdb::val indexVal, event_id;
- size_t index = 0;
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ if (index == std::numeric_limits<uint64_t>::max()) {
+ if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
+ index = *indexVal.data<uint64_t>();
+ } else {
+ messages.end_of_cache = true;
+ return messages;
+ }
+ } else {
+ if (cursor.get(indexVal, event_id, MDB_SET)) {
+ index = *indexVal.data<uint64_t>();
+ } else {
+ messages.end_of_cache = true;
+ return messages;
+ }
+ }
- while (cursor.get(timestamp, msg, MDB_NEXT) && index < MAX_RESTORED_MESSAGES) {
- auto obj = json::parse(msg);
+ int counter = 0;
- if (obj.count("event") == 0 || obj.count("token") == 0)
+ bool ret;
+ while ((ret = cursor.get(indexVal,
+ event_id,
+ counter == 0 ? (forward ? MDB_FIRST : MDB_LAST)
+ : (forward ? MDB_NEXT : MDB_PREV))) &&
+ counter++ < BATCH_SIZE) {
+ lmdb::val event;
+ bool success = lmdb::dbi_get(txn, eventsDb, event_id, event);
+ if (!success)
continue;
- mtx::events::collections::TimelineEvent event;
- mtx::events::collections::from_json(obj.at("event"), event);
-
- index += 1;
+ mtx::events::collections::TimelineEvent te;
+ try {
+ mtx::events::collections::from_json(
+ json::parse(std::string_view(event.data(), event.size())), te);
+ } catch (std::exception &e) {
+ nhlog::db()->error("Failed to parse message from cache {}", e.what());
+ continue;
+ }
- timeline.events.push_back(event.data);
- timeline.prev_batch = obj.at("token").get<std::string>();
+ messages.timeline.events.push_back(std::move(te.data));
}
cursor.close();
- std::reverse(timeline.events.begin(), timeline.events.end());
+ // std::reverse(timeline.events.begin(), timeline.events.end());
+ messages.next_index = *indexVal.data<uint64_t>();
+ messages.end_of_cache = !ret;
+
+ return messages;
+}
+
+std::optional<mtx::events::collections::TimelineEvent>
+Cache::getEvent(const std::string &room_id, const std::string &event_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ auto eventsDb = getEventsDb(txn, room_id);
+
+ lmdb::val event{};
+ bool success = lmdb::dbi_get(txn, eventsDb, lmdb::val(event_id), event);
+ if (!success)
+ return {};
- return timeline;
+ mtx::events::collections::TimelineEvent te;
+ try {
+ mtx::events::collections::from_json(
+ json::parse(std::string_view(event.data(), event.size())), te);
+ } catch (std::exception &e) {
+ nhlog::db()->error("Failed to parse message from cache {}", e.what());
+ return std::nullopt;
+ }
+
+ return te;
+}
+void
+Cache::storeEvent(const std::string &room_id,
+ const std::string &event_id,
+ const mtx::events::collections::TimelineEvent &event)
+{
+ auto txn = lmdb::txn::begin(env_);
+ auto eventsDb = getEventsDb(txn, room_id);
+ auto event_json = mtx::accessors::serialize_event(event.data);
+ lmdb::dbi_put(txn, eventsDb, lmdb::val(event_id), lmdb::val(event_json.dump()));
+ txn.commit();
+}
+
+std::vector<std::string>
+Cache::relatedEvents(const std::string &room_id, const std::string &event_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ auto relationsDb = getRelationsDb(txn, room_id);
+
+ std::vector<std::string> related_ids;
+
+ auto related_cursor = lmdb::cursor::open(txn, relationsDb);
+ lmdb::val related_to = event_id, related_event;
+ bool first = true;
+
+ try {
+ if (!related_cursor.get(related_to, related_event, MDB_SET))
+ return {};
+
+ while (related_cursor.get(
+ related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+ first = false;
+ if (event_id != std::string_view(related_to.data(), related_to.size()))
+ break;
+
+ related_ids.emplace_back(related_event.data(), related_event.size());
+ }
+ } catch (const lmdb::error &e) {
+ nhlog::db()->error("related events error: {}", e.what());
+ }
+
+ return related_ids;
}
QMap<QString, RoomInfo>
@@ -1306,55 +1504,113 @@ Cache::roomInfo(bool withInvites)
std::string
Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id)
{
- auto db = getMessagesDb(txn, room_id);
+ auto orderDb = getOrderToMessageDb(txn, room_id);
+
+ lmdb::val indexVal, val;
- if (db.size(txn) == 0)
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ if (!cursor.get(indexVal, val, MDB_LAST)) {
return {};
+ }
- std::string timestamp, msg;
+ return std::string(val.data(), val.size());
+}
- auto cursor = lmdb::cursor::open(txn, db);
- while (cursor.get(timestamp, msg, MDB_NEXT)) {
- auto obj = json::parse(msg);
+std::optional<Cache::TimelineRange>
+Cache::getTimelineRange(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ lmdb::dbi orderDb{0};
+ try {
+ orderDb = getOrderToMessageDb(txn, room_id);
+ } catch (lmdb::runtime_error &e) {
+ nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
+ room_id,
+ e.what());
+ return {};
+ }
- if (obj.count("event") == 0)
- continue;
+ lmdb::val indexVal, val;
- cursor.close();
- return obj["event"]["event_id"];
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ if (!cursor.get(indexVal, val, MDB_LAST)) {
+ return {};
}
- cursor.close();
- return {};
+ TimelineRange range{};
+ range.last = *indexVal.data<uint64_t>();
+
+ if (!cursor.get(indexVal, val, MDB_FIRST)) {
+ return {};
+ }
+ range.first = *indexVal.data<uint64_t>();
+
+ return range;
+}
+std::optional<uint64_t>
+Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ auto orderDb = getMessageToOrderDb(txn, room_id);
+
+ lmdb::val indexVal{event_id.data(), event_id.size()}, val;
+
+ bool success = lmdb::dbi_get(txn, orderDb, indexVal, val);
+ if (!success) {
+ return {};
+ }
+
+ return *val.data<uint64_t>();
+}
+
+std::optional<std::string>
+Cache::getTimelineEventId(const std::string &room_id, uint64_t index)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ auto orderDb = getOrderToMessageDb(txn, room_id);
+
+ lmdb::val indexVal{&index, sizeof(index)}, val;
+
+ bool success = lmdb::dbi_get(txn, orderDb, indexVal, val);
+ if (!success) {
+ return {};
+ }
+
+ return std::string(val.data(), val.size());
}
DescInfo
Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
{
- auto db = getMessagesDb(txn, room_id);
-
- if (db.size(txn) == 0)
+ auto orderDb = getOrderToMessageDb(txn, room_id);
+ auto eventsDb = getEventsDb(txn, room_id);
+ if (orderDb.size(txn) == 0)
return DescInfo{};
- std::string timestamp, msg;
-
const auto local_user = utils::localUser();
DescInfo fallbackDesc{};
- auto cursor = lmdb::cursor::open(txn, db);
- while (cursor.get(timestamp, msg, MDB_NEXT)) {
- auto obj = json::parse(msg);
+ lmdb::val indexVal, event_id;
- if (obj.count("event") == 0)
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ bool first = true;
+ while (cursor.get(indexVal, event_id, first ? MDB_LAST : MDB_PREV)) {
+ first = false;
+
+ lmdb::val event;
+ bool success = lmdb::dbi_get(txn, eventsDb, event_id, event);
+ if (!success)
continue;
- if (fallbackDesc.event_id.isEmpty() && obj["event"]["type"] == "m.room.member" &&
- obj["event"]["state_key"] == local_user.toStdString() &&
- obj["event"]["content"]["membership"] == "join") {
- uint64_t ts = obj["event"]["origin_server_ts"];
+ auto obj = json::parse(std::string_view(event.data(), event.size()));
+
+ if (fallbackDesc.event_id.isEmpty() && obj["type"] == "m.room.member" &&
+ obj["state_key"] == local_user.toStdString() &&
+ obj["content"]["membership"] == "join") {
+ uint64_t ts = obj["origin_server_ts"];
auto time = QDateTime::fromMSecsSinceEpoch(ts);
- fallbackDesc = DescInfo{QString::fromStdString(obj["event"]["event_id"]),
+ fallbackDesc = DescInfo{QString::fromStdString(obj["event_id"]),
local_user,
tr("You joined this room."),
utils::descriptiveTime(time),
@@ -1362,20 +1618,17 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
time};
}
- if (!(obj["event"]["type"] == "m.room.message" ||
- obj["event"]["type"] == "m.sticker" ||
- obj["event"]["type"] == "m.call.invite" ||
- obj["event"]["type"] == "m.call.answer" ||
- obj["event"]["type"] == "m.call.hangup" ||
- obj["event"]["type"] == "m.room.encrypted"))
+ if (!(obj["type"] == "m.room.message" || obj["type"] == "m.sticker" ||
+ obj["type"] == "m.call.invite" || obj["type"] == "m.call.answer" ||
+ obj["type"] == "m.call.hangup" || obj["type"] == "m.room.encrypted"))
continue;
- mtx::events::collections::TimelineEvent event;
- mtx::events::collections::from_json(obj.at("event"), event);
+ mtx::events::collections::TimelineEvent te;
+ mtx::events::collections::from_json(obj, te);
cursor.close();
return utils::getMessageDescription(
- event.data, local_user, QString::fromStdString(room_id));
+ te.data, local_user, QString::fromStdString(room_id));
}
cursor.close();
@@ -1417,7 +1670,7 @@ Cache::getRoomAvatarUrl(lmdb::txn &txn,
if (res) {
try {
StateEvent<Avatar> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
if (!msg.content.url.empty())
return QString::fromStdString(msg.content.url);
@@ -1467,7 +1720,8 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
if (res) {
try {
- StateEvent<Name> msg = json::parse(std::string(event.data(), event.size()));
+ StateEvent<Name> msg =
+ json::parse(std::string_view(event.data(), event.size()));
if (!msg.content.name.empty())
return QString::fromStdString(msg.content.name);
@@ -1482,7 +1736,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
if (res) {
try {
StateEvent<CanonicalAlias> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
if (!msg.content.alias.empty())
return QString::fromStdString(msg.content.alias);
@@ -1545,7 +1799,7 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
if (res) {
try {
StateEvent<state::JoinRules> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
return msg.content.join_rule;
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
@@ -1567,7 +1821,7 @@ Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
if (res) {
try {
StateEvent<GuestAccess> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
return msg.content.guest_access == AccessState::CanJoin;
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse m.room.guest_access event: {}",
@@ -1590,7 +1844,7 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
if (res) {
try {
StateEvent<Topic> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
if (!msg.content.topic.empty())
return QString::fromStdString(msg.content.topic);
@@ -1615,7 +1869,7 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
if (res) {
try {
StateEvent<Create> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
if (!msg.content.room_version.empty())
return QString::fromStdString(msg.content.room_version);
@@ -1641,7 +1895,7 @@ Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &members
if (res) {
try {
StrippedEvent<state::Name> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
return QString::fromStdString(msg.content.name);
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
@@ -1683,7 +1937,7 @@ Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &me
if (res) {
try {
StrippedEvent<state::Avatar> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
return QString::fromStdString(msg.content.url);
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
@@ -1725,7 +1979,7 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
if (res) {
try {
StrippedEvent<Topic> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
return QString::fromStdString(msg.content.topic);
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
@@ -1756,7 +2010,7 @@ Cache::getRoomAvatar(const std::string &room_id)
std::string media_url;
try {
- RoomInfo info = json::parse(std::string(response.data(), response.size()));
+ RoomInfo info = json::parse(std::string_view(response.data(), response.size()));
media_url = std::move(info.avatar_url);
if (media_url.empty()) {
@@ -1953,30 +2207,438 @@ Cache::isRoomMember(const std::string &user_id, const std::string &room_id)
}
void
+Cache::savePendingMessage(const std::string &room_id,
+ const mtx::events::collections::TimelineEvent &message)
+{
+ auto txn = lmdb::txn::begin(env_);
+
+ mtx::responses::Timeline timeline;
+ timeline.events.push_back(message.data);
+ saveTimelineMessages(txn, room_id, timeline);
+
+ auto pending = getPendingMessagesDb(txn, room_id);
+
+ int64_t now = QDateTime::currentMSecsSinceEpoch();
+ lmdb::dbi_put(txn,
+ pending,
+ lmdb::val(&now, sizeof(now)),
+ lmdb::val(mtx::accessors::event_id(message.data)));
+
+ txn.commit();
+}
+
+std::optional<mtx::events::collections::TimelineEvent>
+Cache::firstPendingMessage(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_);
+ auto pending = getPendingMessagesDb(txn, room_id);
+
+ {
+ auto pendingCursor = lmdb::cursor::open(txn, pending);
+ lmdb::val tsIgnored, pendingTxn;
+ while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+ auto eventsDb = getEventsDb(txn, room_id);
+ lmdb::val event;
+ if (!lmdb::dbi_get(txn, eventsDb, pendingTxn, event)) {
+ lmdb::dbi_del(txn, pending, tsIgnored, pendingTxn);
+ continue;
+ }
+
+ try {
+ mtx::events::collections::TimelineEvent te;
+ mtx::events::collections::from_json(
+ json::parse(std::string_view(event.data(), event.size())), te);
+
+ pendingCursor.close();
+ txn.commit();
+ return te;
+ } catch (std::exception &e) {
+ nhlog::db()->error("Failed to parse message from cache {}",
+ e.what());
+ lmdb::dbi_del(txn, pending, tsIgnored, pendingTxn);
+ continue;
+ }
+ }
+ }
+
+ txn.commit();
+
+ return std::nullopt;
+}
+
+void
+Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id)
+{
+ auto txn = lmdb::txn::begin(env_);
+ auto pending = getPendingMessagesDb(txn, room_id);
+
+ {
+ auto pendingCursor = lmdb::cursor::open(txn, pending);
+ lmdb::val tsIgnored, pendingTxn;
+ while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+ if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
+ lmdb::cursor_del(pendingCursor);
+ }
+ }
+
+ txn.commit();
+}
+
+void
Cache::saveTimelineMessages(lmdb::txn &txn,
const std::string &room_id,
const mtx::responses::Timeline &res)
{
- auto db = getMessagesDb(txn, room_id);
+ if (res.events.empty())
+ return;
+
+ auto eventsDb = getEventsDb(txn, room_id);
+ auto relationsDb = getRelationsDb(txn, room_id);
+
+ auto orderDb = getEventOrderDb(txn, room_id);
+ auto evToOrderDb = getEventToOrderDb(txn, room_id);
+ auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+ auto order2msgDb = getOrderToMessageDb(txn, room_id);
+ auto pending = getPendingMessagesDb(txn, room_id);
+
+ if (res.limited) {
+ lmdb::dbi_drop(txn, orderDb, false);
+ lmdb::dbi_drop(txn, evToOrderDb, false);
+ lmdb::dbi_drop(txn, msg2orderDb, false);
+ lmdb::dbi_drop(txn, order2msgDb, false);
+ lmdb::dbi_drop(txn, pending, true);
+ }
using namespace mtx::events;
using namespace mtx::events::state;
+ lmdb::val indexVal, val;
+ uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ if (cursor.get(indexVal, val, MDB_LAST)) {
+ index = *indexVal.data<int64_t>();
+ }
+
+ uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
+ auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+ if (msgCursor.get(indexVal, val, MDB_LAST)) {
+ msgIndex = *indexVal.data<uint64_t>();
+ }
+
+ bool first = true;
for (const auto &e : res.events) {
- if (std::holds_alternative<RedactionEvent<msg::Redaction>>(e))
+ auto event = mtx::accessors::serialize_event(e);
+ auto txn_id = mtx::accessors::transaction_id(e);
+
+ std::string event_id_val = event.value("event_id", "");
+ if (event_id_val.empty()) {
+ nhlog::db()->error("Event without id!");
continue;
+ }
+
+ lmdb::val event_id = event_id_val;
+
+ json orderEntry = json::object();
+ orderEntry["event_id"] = event_id_val;
+ if (first && !res.prev_batch.empty())
+ orderEntry["prev_batch"] = res.prev_batch;
+
+ lmdb::val txn_order;
+ if (!txn_id.empty() &&
+ lmdb::dbi_get(txn, evToOrderDb, lmdb::val(txn_id), txn_order)) {
+ lmdb::dbi_put(txn, eventsDb, event_id, lmdb::val(event.dump()));
+ lmdb::dbi_del(txn, eventsDb, lmdb::val(txn_id));
+
+ lmdb::val msg_txn_order;
+ if (lmdb::dbi_get(txn, msg2orderDb, lmdb::val(txn_id), msg_txn_order)) {
+ lmdb::dbi_put(txn, order2msgDb, msg_txn_order, event_id);
+ lmdb::dbi_put(txn, msg2orderDb, event_id, msg_txn_order);
+ lmdb::dbi_del(txn, msg2orderDb, lmdb::val(txn_id));
+ }
+
+ lmdb::dbi_put(txn, orderDb, txn_order, lmdb::val(orderEntry.dump()));
+ lmdb::dbi_put(txn, evToOrderDb, event_id, txn_order);
+ lmdb::dbi_del(txn, evToOrderDb, lmdb::val(txn_id));
+
+ if (event.contains("content") &&
+ event["content"].contains("m.relates_to")) {
+ auto temp = event["content"]["m.relates_to"];
+ std::string relates_to = temp.contains("m.in_reply_to")
+ ? temp["m.in_reply_to"]["event_id"]
+ : temp["event_id"];
+
+ if (!relates_to.empty()) {
+ lmdb::dbi_del(txn,
+ relationsDb,
+ lmdb::val(relates_to),
+ lmdb::val(txn_id));
+ lmdb::dbi_put(
+ txn, relationsDb, lmdb::val(relates_to), event_id);
+ }
+ }
+
+ auto pendingCursor = lmdb::cursor::open(txn, pending);
+ lmdb::val tsIgnored, pendingTxn;
+ while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+ if (std::string_view(pendingTxn.data(), pendingTxn.size()) ==
+ txn_id)
+ lmdb::cursor_del(pendingCursor);
+ }
+ } else if (auto redaction =
+ std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
+ &e)) {
+ if (redaction->redacts.empty())
+ continue;
+
+ lmdb::val oldEvent;
+ bool success =
+ lmdb::dbi_get(txn, eventsDb, lmdb::val(redaction->redacts), oldEvent);
+ if (!success)
+ continue;
- json obj = json::object();
+ mtx::events::collections::TimelineEvent te;
+ try {
+ mtx::events::collections::from_json(
+ json::parse(std::string_view(oldEvent.data(), oldEvent.size())),
+ te);
+ // overwrite the content and add redation data
+ std::visit(
+ [redaction](auto &ev) {
+ ev.unsigned_data.redacted_because = *redaction;
+ ev.unsigned_data.redacted_by = redaction->event_id;
+ },
+ te.data);
+ event = mtx::accessors::serialize_event(te.data);
+ event["content"].clear();
+
+ } catch (std::exception &e) {
+ nhlog::db()->error("Failed to parse message from cache {}",
+ e.what());
+ continue;
+ }
+
+ lmdb::dbi_put(
+ txn, eventsDb, lmdb::val(redaction->redacts), lmdb::val(event.dump()));
+ lmdb::dbi_put(txn,
+ eventsDb,
+ lmdb::val(redaction->event_id),
+ lmdb::val(json(*redaction).dump()));
+ } else {
+ lmdb::dbi_put(txn, eventsDb, event_id, lmdb::val(event.dump()));
+
+ ++index;
+
+ first = false;
+
+ nhlog::db()->debug("saving '{}'", orderEntry.dump());
+
+ lmdb::cursor_put(cursor.handle(),
+ lmdb::val(&index, sizeof(index)),
+ lmdb::val(orderEntry.dump()),
+ MDB_APPEND);
+ lmdb::dbi_put(txn, evToOrderDb, event_id, lmdb::val(&index, sizeof(index)));
+
+ // TODO(Nico): Allow blacklisting more event types in UI
+ if (!isHiddenEvent(e, room_id)) {
+ ++msgIndex;
+ lmdb::cursor_put(msgCursor.handle(),
+ lmdb::val(&msgIndex, sizeof(msgIndex)),
+ event_id,
+ MDB_APPEND);
+
+ lmdb::dbi_put(txn,
+ msg2orderDb,
+ event_id,
+ lmdb::val(&msgIndex, sizeof(msgIndex)));
+ }
+
+ if (event.contains("content") &&
+ event["content"].contains("m.relates_to")) {
+ auto temp = event["content"]["m.relates_to"];
+ std::string relates_to = temp.contains("m.in_reply_to")
+ ? temp["m.in_reply_to"]["event_id"]
+ : temp["event_id"];
+
+ if (!relates_to.empty())
+ lmdb::dbi_put(
+ txn, relationsDb, lmdb::val(relates_to), event_id);
+ }
+ }
+ }
+}
+
+uint64_t
+Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res)
+{
+ auto txn = lmdb::txn::begin(env_);
+ auto eventsDb = getEventsDb(txn, room_id);
+ auto relationsDb = getRelationsDb(txn, room_id);
+
+ auto orderDb = getEventOrderDb(txn, room_id);
+ auto evToOrderDb = getEventToOrderDb(txn, room_id);
+ auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+ auto order2msgDb = getOrderToMessageDb(txn, room_id);
+
+ lmdb::val indexVal, val;
+ uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
+ {
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+ if (cursor.get(indexVal, val, MDB_FIRST)) {
+ index = *indexVal.data<uint64_t>();
+ }
+ }
+
+ uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
+ {
+ auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+ if (msgCursor.get(indexVal, val, MDB_FIRST)) {
+ msgIndex = *indexVal.data<uint64_t>();
+ }
+ }
+
+ if (res.chunk.empty())
+ return index;
+
+ std::string event_id_val;
+ for (const auto &e : res.chunk) {
+ if (std::holds_alternative<
+ mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e))
+ continue;
- obj["event"] = mtx::accessors::serialize_event(e);
- obj["token"] = res.prev_batch;
+ auto event = mtx::accessors::serialize_event(e);
+ event_id_val = event["event_id"].get<std::string>();
+ lmdb::val event_id = event_id_val;
+ lmdb::dbi_put(txn, eventsDb, event_id, lmdb::val(event.dump()));
+
+ --index;
+
+ json orderEntry = json::object();
+ orderEntry["event_id"] = event_id_val;
+
+ nhlog::db()->debug("saving '{}'", orderEntry.dump());
lmdb::dbi_put(
- txn,
- db,
- lmdb::val(std::to_string(obj["event"]["origin_server_ts"].get<uint64_t>())),
- lmdb::val(obj.dump()));
+ txn, orderDb, lmdb::val(&index, sizeof(index)), lmdb::val(orderEntry.dump()));
+ lmdb::dbi_put(txn, evToOrderDb, event_id, lmdb::val(&index, sizeof(index)));
+
+ // TODO(Nico): Allow blacklisting more event types in UI
+ if (!isHiddenEvent(e, room_id)) {
+ --msgIndex;
+ lmdb::dbi_put(
+ txn, order2msgDb, lmdb::val(&msgIndex, sizeof(msgIndex)), event_id);
+
+ lmdb::dbi_put(
+ txn, msg2orderDb, event_id, lmdb::val(&msgIndex, sizeof(msgIndex)));
+ }
+
+ if (event.contains("content") && event["content"].contains("m.relates_to")) {
+ auto temp = event["content"]["m.relates_to"];
+ std::string relates_to = temp.contains("m.in_reply_to")
+ ? temp["m.in_reply_to"]["event_id"]
+ : temp["event_id"];
+
+ if (!relates_to.empty())
+ lmdb::dbi_put(txn, relationsDb, lmdb::val(relates_to), event_id);
+ }
}
+
+ json orderEntry = json::object();
+ orderEntry["event_id"] = event_id_val;
+ orderEntry["prev_batch"] = res.end;
+ lmdb::dbi_put(txn, orderDb, lmdb::val(&index, sizeof(index)), lmdb::val(orderEntry.dump()));
+ nhlog::db()->debug("saving '{}'", orderEntry.dump());
+
+ txn.commit();
+
+ return msgIndex;
+}
+
+void
+Cache::clearTimeline(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_);
+ auto eventsDb = getEventsDb(txn, room_id);
+ auto relationsDb = getRelationsDb(txn, room_id);
+
+ auto orderDb = getEventOrderDb(txn, room_id);
+ auto evToOrderDb = getEventToOrderDb(txn, room_id);
+ auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+ auto order2msgDb = getOrderToMessageDb(txn, room_id);
+
+ lmdb::val indexVal, val;
+ auto cursor = lmdb::cursor::open(txn, orderDb);
+
+ bool start = true;
+ bool passed_pagination_token = false;
+ while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
+ start = false;
+ json obj;
+
+ try {
+ obj = json::parse(std::string_view(val.data(), val.size()));
+ } catch (std::exception &) {
+ // workaround bug in the initial db format, where we sometimes didn't store
+ // json...
+ obj = {{"event_id", std::string(val.data(), val.size())}};
+ }
+
+ if (passed_pagination_token) {
+ if (obj.count("event_id") != 0) {
+ lmdb::val event_id = obj["event_id"].get<std::string>();
+ lmdb::dbi_del(txn, evToOrderDb, event_id);
+ lmdb::dbi_del(txn, eventsDb, event_id);
+
+ lmdb::dbi_del(txn, relationsDb, event_id);
+
+ lmdb::val order{};
+ bool exists = lmdb::dbi_get(txn, msg2orderDb, event_id, order);
+ if (exists) {
+ lmdb::dbi_del(txn, order2msgDb, order);
+ lmdb::dbi_del(txn, msg2orderDb, event_id);
+ }
+ }
+ lmdb::cursor_del(cursor);
+ } else {
+ if (obj.count("prev_batch") != 0)
+ passed_pagination_token = true;
+ }
+ }
+
+ auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+ start = true;
+ while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
+ start = false;
+
+ lmdb::val eventId;
+ bool innerStart = true;
+ bool found = false;
+ while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) {
+ innerStart = false;
+
+ json obj;
+ try {
+ obj = json::parse(std::string_view(eventId.data(), eventId.size()));
+ } catch (std::exception &) {
+ obj = {{"event_id", std::string(eventId.data(), eventId.size())}};
+ }
+
+ if (obj["event_id"] == std::string(val.data(), val.size())) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ break;
+ }
+
+ do {
+ lmdb::cursor_del(msgCursor);
+ } while (msgCursor.get(indexVal, val, MDB_PREV));
+
+ cursor.close();
+ msgCursor.close();
+ txn.commit();
}
mtx::responses::Notifications
@@ -2111,34 +2773,60 @@ Cache::getRoomIds(lmdb::txn &txn)
void
Cache::deleteOldMessages()
{
+ lmdb::val indexVal, val;
+
auto txn = lmdb::txn::begin(env_);
auto room_ids = getRoomIds(txn);
- for (const auto &id : room_ids) {
- auto msg_db = getMessagesDb(txn, id);
+ for (const auto &room_id : room_ids) {
+ auto orderDb = getEventOrderDb(txn, room_id);
+ auto evToOrderDb = getEventToOrderDb(txn, room_id);
+ auto o2m = getOrderToMessageDb(txn, room_id);
+ auto m2o = getMessageToOrderDb(txn, room_id);
+ auto eventsDb = getEventsDb(txn, room_id);
+ auto relationsDb = getRelationsDb(txn, room_id);
+ auto cursor = lmdb::cursor::open(txn, orderDb);
- std::string ts, event;
- uint64_t idx = 0;
+ uint64_t first, last;
+ if (cursor.get(indexVal, val, MDB_LAST)) {
+ last = *indexVal.data<uint64_t>();
+ } else {
+ continue;
+ }
+ if (cursor.get(indexVal, val, MDB_FIRST)) {
+ first = *indexVal.data<uint64_t>();
+ } else {
+ continue;
+ }
- const auto db_size = msg_db.size(txn);
- if (db_size <= 3 * MAX_RESTORED_MESSAGES)
+ size_t message_count = static_cast<size_t>(last - first);
+ if (message_count < MAX_RESTORED_MESSAGES)
continue;
- nhlog::db()->info("[{}] message count: {}", id, db_size);
+ bool start = true;
+ while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) &&
+ message_count-- > MAX_RESTORED_MESSAGES) {
+ start = false;
+ auto obj = json::parse(std::string_view(val.data(), val.size()));
- auto cursor = lmdb::cursor::open(txn, msg_db);
- while (cursor.get(ts, event, MDB_NEXT)) {
- idx += 1;
+ if (obj.count("event_id") != 0) {
+ lmdb::val event_id = obj["event_id"].get<std::string>();
+ lmdb::dbi_del(txn, evToOrderDb, event_id);
+ lmdb::dbi_del(txn, eventsDb, event_id);
- if (idx > MAX_RESTORED_MESSAGES)
- lmdb::cursor_del(cursor);
- }
+ lmdb::dbi_del(txn, relationsDb, event_id);
+ lmdb::val order{};
+ bool exists = lmdb::dbi_get(txn, m2o, event_id, order);
+ if (exists) {
+ lmdb::dbi_del(txn, o2m, order);
+ lmdb::dbi_del(txn, m2o, event_id);
+ }
+ }
+ lmdb::cursor_del(cursor);
+ }
cursor.close();
-
- nhlog::db()->info("[{}] updated message count: {}", id, msg_db.size(txn));
}
-
txn.commit();
}
@@ -2172,7 +2860,7 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
if (res) {
try {
StateEvent<PowerLevels> msg =
- json::parse(std::string(event.data(), event.size()));
+ json::parse(std::string_view(event.data(), event.size()));
user_level = msg.content.user_level(user_id);
@@ -2277,6 +2965,9 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id)
mtx::presence::PresenceState
Cache::presenceState(const std::string &user_id)
{
+ if (user_id.empty())
+ return {};
+
lmdb::val presenceVal;
auto txn = lmdb::txn::begin(env_);
@@ -2287,7 +2978,7 @@ Cache::presenceState(const std::string &user_id)
if (res) {
mtx::events::presence::Presence presence =
- json::parse(std::string(presenceVal.data(), presenceVal.size()));
+ json::parse(std::string_view(presenceVal.data(), presenceVal.size()));
state = presence.presence;
}
@@ -2299,6 +2990,9 @@ Cache::presenceState(const std::string &user_id)
std::string
Cache::statusMessage(const std::string &user_id)
{
+ if (user_id.empty())
+ return {};
+
lmdb::val presenceVal;
auto txn = lmdb::txn::begin(env_);
@@ -2309,7 +3003,7 @@ Cache::statusMessage(const std::string &user_id)
if (res) {
mtx::events::presence::Presence presence =
- json::parse(std::string(presenceVal.data(), presenceVal.size()));
+ json::parse(std::string_view(presenceVal.data(), presenceVal.size()));
status_msg = presence.status_msg;
}
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 892b66a5..d3ec6ee0 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -18,6 +18,7 @@
#pragma once
+#include <limits>
#include <optional>
#include <QDateTime>
@@ -38,9 +39,6 @@
#include "CacheCryptoStructs.h"
#include "CacheStructs.h"
-int
-numeric_key_comparison(const MDB_val *a, const MDB_val *b);
-
class Cache : public QObject
{
Q_OBJECT
@@ -172,6 +170,47 @@ public:
//! Add all notifications containing a user mention to the db.
void saveTimelineMentions(const mtx::responses::Notifications &res);
+ //! retrieve events in timeline and related functions
+ struct Messages
+ {
+ mtx::responses::Timeline timeline;
+ uint64_t next_index;
+ bool end_of_cache = false;
+ };
+ Messages getTimelineMessages(lmdb::txn &txn,
+ const std::string &room_id,
+ uint64_t index = std::numeric_limits<uint64_t>::max(),
+ bool forward = false);
+
+ std::optional<mtx::events::collections::TimelineEvent> getEvent(
+ const std::string &room_id,
+ const std::string &event_id);
+ void storeEvent(const std::string &room_id,
+ const std::string &event_id,
+ const mtx::events::collections::TimelineEvent &event);
+ std::vector<std::string> relatedEvents(const std::string &room_id,
+ const std::string &event_id);
+
+ struct TimelineRange
+ {
+ uint64_t first, last;
+ };
+ std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
+ std::optional<uint64_t> getTimelineIndex(const std::string &room_id,
+ std::string_view event_id);
+ std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
+
+ std::string previousBatchToken(const std::string &room_id);
+ uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
+ void savePendingMessage(const std::string &room_id,
+ const mtx::events::collections::TimelineEvent &message);
+ std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage(
+ const std::string &room_id);
+ void removePendingStatus(const std::string &room_id, const std::string &txn_id);
+
+ //! clear timeline keeping only the latest batch
+ void clearTimeline(const std::string &room_id);
+
//! Remove old unused data.
void deleteOldMessages();
void deleteOldData() noexcept;
@@ -250,8 +289,6 @@ private:
const std::string &room_id,
const mtx::responses::Timeline &res);
- mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id);
-
//! Remove a room from the cache.
// void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
template<class T>
@@ -402,13 +439,46 @@ private:
return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
}
- lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id)
+ lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
+ {
+ return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
+ }
+
+ lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
+ {
+ return lmdb::dbi::open(
+ txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+ }
+
+ // inverse of EventOrderDb
+ lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
- auto db =
- lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE);
- lmdb::dbi_set_compare(txn, db, numeric_key_comparison);
+ return lmdb::dbi::open(
+ txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
+ }
+
+ lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
+ {
+ return lmdb::dbi::open(
+ txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
+ }
+
+ lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
+ {
+ return lmdb::dbi::open(
+ txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+ }
+
+ lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
+ {
+ return lmdb::dbi::open(
+ txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+ }
- return db;
+ lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
+ {
+ return lmdb::dbi::open(
+ txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
}
lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 84a5e4d3..e55b3eca 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -165,6 +165,11 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
trySync();
});
+ connect(text_input_,
+ &TextInputWidget::clearRoomTimeline,
+ view_manager_,
+ &TimelineViewManager::clearCurrentRoomTimeline);
+
connect(
new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
if (isVisible())
@@ -254,7 +259,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView);
connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
- view_manager_->addRoom(room_id);
joinRoom(room_id);
room_list_->removeRoom(room_id, currentRoom() == room_id);
});
@@ -323,17 +327,15 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
.toStdString();
member.membership = mtx::events::state::Membership::Join;
- http::client()
- ->send_state_event<mtx::events::state::Member,
- mtx::events::EventType::RoomMember>(
- currentRoom().toStdString(),
- http::client()->user_id().to_string(),
- member,
- [](mtx::responses::EventId, mtx::http::RequestErr err) {
- if (err)
- nhlog::net()->error("Failed to set room displayname: {}",
- err->matrix_error.error);
- });
+ http::client()->send_state_event(
+ currentRoom().toStdString(),
+ http::client()->user_id().to_string(),
+ member,
+ [](mtx::responses::EventId, mtx::http::RequestErr err) {
+ if (err)
+ nhlog::net()->error("Failed to set room displayname: {}",
+ err->matrix_error.error);
+ });
});
connect(
@@ -584,12 +586,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
emit notificationsRetrieved(std::move(res));
});
});
- connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync, Qt::QueuedConnection);
- connect(this,
- &ChatPage::syncTags,
- communitiesList_,
- &CommunitiesList::syncTags,
- Qt::QueuedConnection);
+ connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
+ connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags);
connect(
this, &ChatPage::syncTopBar, this, [this](const std::map<QString, RoomInfo> &updates) {
if (updates.find(currentRoom()) != updates.end())
@@ -614,6 +612,12 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
[this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
Qt::QueuedConnection);
+ connect(this,
+ &ChatPage::newSyncResponse,
+ this,
+ &ChatPage::handleSyncResponse,
+ Qt::QueuedConnection);
+
connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
connectCallMessage<mtx::events::msg::CallInvite>();
@@ -841,43 +845,39 @@ ChatPage::loadStateFromCache()
nhlog::db()->info("restoring state from cache");
- getProfileInfo();
+ try {
+ cache::restoreSessions();
+ olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
- QtConcurrent::run([this]() {
- try {
- cache::restoreSessions();
- olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
+ cache::populateMembers();
- cache::populateMembers();
+ emit initializeEmptyViews(cache::roomMessages());
+ emit initializeRoomList(cache::roomInfo());
+ emit initializeMentions(cache::getTimelineMentions());
+ emit syncTags(cache::roomInfo().toStdMap());
- emit initializeEmptyViews(cache::roomMessages());
- emit initializeRoomList(cache::roomInfo());
- emit initializeMentions(cache::getTimelineMentions());
- emit syncTags(cache::roomInfo().toStdMap());
+ cache::calculateRoomReadStatus();
- cache::calculateRoomReadStatus();
+ } catch (const mtx::crypto::olm_exception &e) {
+ nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
+ emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
+ return;
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("failed to restore cache: {}", e.what());
+ emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+ return;
+ } catch (const json::exception &e) {
+ nhlog::db()->critical("failed to parse cache data: {}", e.what());
+ return;
+ }
- } catch (const mtx::crypto::olm_exception &e) {
- nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
- emit dropToLoginPageCb(
- tr("Failed to restore OLM account. Please login again."));
- return;
- } catch (const lmdb::error &e) {
- nhlog::db()->critical("failed to restore cache: {}", e.what());
- emit dropToLoginPageCb(
- tr("Failed to restore save data. Please login again."));
- return;
- } catch (const json::exception &e) {
- nhlog::db()->critical("failed to parse cache data: {}", e.what());
- return;
- }
+ nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519);
+ nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
- nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519);
- nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+ getProfileInfo();
- // Start receiving events.
- emit trySyncCb();
- });
+ // Start receiving events.
+ emit trySyncCb();
}
void
@@ -1056,6 +1056,45 @@ ChatPage::startInitialSync()
}
void
+ChatPage::handleSyncResponse(mtx::responses::Sync res)
+{
+ nhlog::net()->debug("sync completed: {}", res.next_batch);
+
+ // Ensure that we have enough one-time keys available.
+ ensureOneTimeKeyCount(res.device_one_time_keys_count);
+
+ // TODO: fine grained error handling
+ try {
+ cache::saveState(res);
+ olm::handle_to_device_messages(res.to_device.events);
+
+ auto updates = cache::roomUpdates(res);
+
+ emit syncTopBar(updates);
+ emit syncRoomlist(updates);
+
+ emit syncUI(res.rooms);
+
+ emit syncTags(cache::roomTagUpdates(res));
+
+ // if we process a lot of syncs (1 every 200ms), this means we clean the
+ // db every 100s
+ static int syncCounter = 0;
+ if (syncCounter++ >= 500) {
+ cache::deleteOldData();
+ syncCounter = 0;
+ }
+ } catch (const lmdb::map_full_error &e) {
+ nhlog::db()->error("lmdb is full: {}", e.what());
+ cache::deleteOldData();
+ } catch (const lmdb::error &e) {
+ nhlog::db()->error("saving sync response: {}", e.what());
+ }
+
+ emit trySyncCb();
+}
+
+void
ChatPage::trySync()
{
mtx::http::SyncOpts opts;
@@ -1072,7 +1111,14 @@ ChatPage::trySync()
}
http::client()->sync(
- opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+ opts,
+ [this, since = cache::nextBatchToken()](const mtx::responses::Sync &res,
+ mtx::http::RequestErr err) {
+ if (since != cache::nextBatchToken()) {
+ nhlog::net()->warn("Duplicate sync, dropping");
+ return;
+ }
+
if (err) {
const auto error = QString::fromStdString(err->matrix_error.error);
const auto msg = tr("Please try to login again: %1").arg(error);
@@ -1094,40 +1140,7 @@ ChatPage::trySync()
return;
}
- nhlog::net()->debug("sync completed: {}", res.next_batch);
-
- // Ensure that we have enough one-time keys available.
- ensureOneTimeKeyCount(res.device_one_time_keys_count);
-
- // TODO: fine grained error handling
- try {
- cache::saveState(res);
- olm::handle_to_device_messages(res.to_device.events);
-
- auto updates = cache::roomUpdates(res);
-
- emit syncTopBar(updates);
- emit syncRoomlist(updates);
-
- emit syncUI(res.rooms);
-
- emit syncTags(cache::roomTagUpdates(res));
-
- // if we process a lot of syncs (1 every 200ms), this means we clean the
- // db every 100s
- static int syncCounter = 0;
- if (syncCounter++ >= 500) {
- cache::deleteOldData();
- syncCounter = 0;
- }
- } catch (const lmdb::map_full_error &e) {
- nhlog::db()->error("lmdb is full: {}", e.what());
- cache::deleteOldData();
- } catch (const lmdb::error &e) {
- nhlog::db()->error("saving sync response: {}", e.what());
- }
-
- emit trySyncCb();
+ emit newSyncResponse(res);
});
}
diff --git a/src/ChatPage.h b/src/ChatPage.h
index fe63c9d9..ba1c56d1 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -140,6 +140,7 @@ signals:
void trySyncCb();
void tryDelayedSyncCb();
void tryInitialSyncCb();
+ void newSyncResponse(mtx::responses::Sync res);
void leftRoom(const QString &room_id);
void initializeRoomList(QMap<QString, RoomInfo>);
@@ -174,6 +175,7 @@ private slots:
void joinRoom(const QString &room);
void sendTypingNotifications();
+ void handleSyncResponse(mtx::responses::Sync res);
private:
static ChatPage *instance_;
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index 7846737b..88612b14 100644
--- a/src/EventAccessors.cpp
+++ b/src/EventAccessors.cpp
@@ -248,6 +248,20 @@ struct EventInReplyTo
}
};
+struct EventRelatesTo
+{
+ template<class Content>
+ using related_ev_id_t = decltype(Content::relates_to.event_id);
+ template<class T>
+ std::string operator()(const mtx::events::Event<T> &e)
+ {
+ if constexpr (is_detected<related_ev_id_t, T>::value) {
+ return e.content.relates_to.event_id;
+ }
+ return "";
+ }
+};
+
struct EventTransactionId
{
template<class T>
@@ -409,6 +423,11 @@ mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents
{
return std::visit(EventInReplyTo{}, event);
}
+std::string
+mtx::accessors::relates_to_event_id(const mtx::events::collections::TimelineEvents &event)
+{
+ return std::visit(EventRelatesTo{}, event);
+}
std::string
mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event)
diff --git a/src/EventAccessors.h b/src/EventAccessors.h
index fa70f3eb..0cdc5f89 100644
--- a/src/EventAccessors.h
+++ b/src/EventAccessors.h
@@ -56,6 +56,8 @@ mimetype(const mtx::events::collections::TimelineEvents &event);
std::string
in_reply_to_event(const mtx::events::collections::TimelineEvents &event);
std::string
+relates_to_event_id(const mtx::events::collections::TimelineEvents &event);
+std::string
transaction_id(const mtx::events::collections::TimelineEvents &event);
int64_t
diff --git a/src/Olm.cpp b/src/Olm.cpp
index 994a3a67..e38e9ef7 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -3,6 +3,7 @@
#include "Olm.h"
#include "Cache.h"
+#include "Cache_p.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
@@ -316,32 +317,36 @@ send_key_request_for(const std::string &room_id,
using namespace mtx::events;
nhlog::crypto()->debug("sending key request: {}", json(e).dump(2));
- auto payload = json{{"action", "request"},
- {"request_id", http::client()->generate_txn_id()},
- {"requesting_device_id", http::client()->device_id()},
- {"body",
- {{"algorithm", MEGOLM_ALGO},
- {"room_id", room_id},
- {"sender_key", e.content.sender_key},
- {"session_id", e.content.session_id}}}};
- json body;
- body["messages"][e.sender] = json::object();
- body["messages"][e.sender][e.content.device_id] = payload;
+ mtx::events::msg::KeyRequest request;
+ request.action = mtx::events::msg::RequestAction::Request;
+ request.algorithm = MEGOLM_ALGO;
+ request.room_id = room_id;
+ request.sender_key = e.content.sender_key;
+ request.session_id = e.content.session_id;
+ request.request_id = "key_request." + http::client()->generate_txn_id();
+ request.requesting_device_id = http::client()->device_id();
- nhlog::crypto()->debug("m.room_key_request: {}", body.dump(2));
+ nhlog::crypto()->debug("m.room_key_request: {}", json(request).dump(2));
- http::client()->send_to_device("m.room_key_request", body, [e](mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->warn("failed to send "
- "send_to_device "
- "message: {}",
- err->matrix_error.error);
- }
+ std::map<mtx::identifiers::User, std::map<std::string, decltype(request)>> body;
+ body[mtx::identifiers::parse<mtx::identifiers::User>(e.sender)][e.content.device_id] =
+ request;
+ body[http::client()->user_id()]["*"] = request;
+
+ http::client()->send_to_device(
+ http::client()->generate_txn_id(), body, [e](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to send "
+ "send_to_device "
+ "message: {}",
+ err->matrix_error.error);
+ }
- nhlog::net()->info(
- "m.room_key_request sent to {}:{}", e.sender, e.content.device_id);
- });
+ nhlog::net()->info("m.room_key_request sent to {}:{} and your own devices",
+ e.sender,
+ e.content.device_id);
+ });
}
void
@@ -551,4 +556,50 @@ send_megolm_key_to_device(const std::string &user_id,
});
}
+DecryptionResult
+decryptEvent(const MegolmSessionIndex &index,
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event)
+{
+ try {
+ if (!cache::client()->inboundMegolmSessionExists(index)) {
+ return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt};
+ }
+ } catch (const lmdb::error &e) {
+ return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
+ }
+
+ // TODO: Lookup index,event_id,origin_server_ts tuple for replay attack errors
+ // TODO: Verify sender_key
+
+ std::string msg_str;
+ try {
+ auto session = cache::client()->getInboundMegolmSession(index);
+ auto res = olm::client()->decrypt_group_message(session, event.content.ciphertext);
+ msg_str = std::string((char *)res.data.data(), res.data.size());
+ } catch (const lmdb::error &e) {
+ return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
+ } catch (const mtx::crypto::olm_exception &e) {
+ return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt};
+ }
+
+ // Add missing fields for the event.
+ json body = json::parse(msg_str);
+ body["event_id"] = event.event_id;
+ body["sender"] = event.sender;
+ body["origin_server_ts"] = event.origin_server_ts;
+ body["unsigned"] = event.unsigned_data;
+
+ // relations are unencrypted in content...
+ if (json old_ev = event; old_ev["content"].count("m.relates_to") != 0)
+ body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
+
+ mtx::events::collections::TimelineEvent te;
+ try {
+ mtx::events::collections::from_json(body, te);
+ } catch (std::exception &e) {
+ return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
+ }
+
+ return {std::nullopt, std::nullopt, std::move(te.data)};
+}
} // namespace olm
diff --git a/src/Olm.h b/src/Olm.h
index 09038ad1..87f4e3ec 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -7,10 +7,30 @@
#include <mtx/events/encrypted.hpp>
#include <mtxclient/crypto/client.hpp>
+#include <CacheCryptoStructs.h>
+
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
namespace olm {
+enum class DecryptionErrorCode
+{
+ MissingSession, // Session was not found, retrieve from backup or request from other devices
+ // and try again
+ DbError, // DB read failed
+ DecryptionFailed, // libolm error
+ ParsingFailed, // Failed to parse the actual event
+ ReplayAttack, // Megolm index reused
+ UnknownFingerprint, // Unknown device Fingerprint
+};
+
+struct DecryptionResult
+{
+ std::optional<DecryptionErrorCode> error;
+ std::optional<std::string> error_message;
+ std::optional<mtx::events::collections::TimelineEvents> event;
+};
+
struct OlmMessage
{
std::string sender_key;
@@ -65,6 +85,10 @@ encrypt_group_message(const std::string &room_id,
const std::string &device_id,
nlohmann::json body);
+DecryptionResult
+decryptEvent(const MegolmSessionIndex &index,
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event);
+
void
mark_keys_as_published();
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index 9af7de26..bdc430f5 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -683,27 +683,29 @@ void
TextInputWidget::command(QString command, QString args)
{
if (command == "me") {
- sendEmoteMessage(args);
+ emit sendEmoteMessage(args);
} else if (command == "join") {
- sendJoinRoomRequest(args);
+ emit sendJoinRoomRequest(args);
} else if (command == "invite") {
- sendInviteRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
+ emit sendInviteRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
} else if (command == "kick") {
- sendKickRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
+ emit sendKickRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
} else if (command == "ban") {
- sendBanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
+ emit sendBanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
} else if (command == "unban") {
- sendUnbanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
+ emit sendUnbanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1));
} else if (command == "roomnick") {
- changeRoomNick(args);
+ emit changeRoomNick(args);
} else if (command == "shrug") {
- sendTextMessage("¯\\_(ツ)_/¯");
+ emit sendTextMessage("¯\\_(ツ)_/¯");
} else if (command == "fliptable") {
- sendTextMessage("(╯°□°)╯︵ ┻━┻");
+ emit sendTextMessage("(╯°□°)╯︵ ┻━┻");
} else if (command == "unfliptable") {
- sendTextMessage(" ┯━┯╭( º _ º╭)");
+ emit sendTextMessage(" ┯━┯╭( º _ º╭)");
} else if (command == "sovietflip") {
- sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\");
+ emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\");
+ } else if (command == "clear-timeline") {
+ emit clearRoomTimeline();
}
}
@@ -735,7 +737,7 @@ TextInputWidget::showUploadSpinner()
topLayout_->removeWidget(sendFileBtn_);
sendFileBtn_->hide();
- topLayout_->insertWidget(0, spinner_);
+ topLayout_->insertWidget(1, spinner_);
spinner_->start();
}
@@ -743,7 +745,7 @@ void
TextInputWidget::hideUploadSpinner()
{
topLayout_->removeWidget(spinner_);
- topLayout_->insertWidget(0, sendFileBtn_);
+ topLayout_->insertWidget(1, sendFileBtn_);
sendFileBtn_->show();
spinner_->stop();
}
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 9e70f498..8cd61b6a 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -180,6 +180,7 @@ private slots:
signals:
void sendTextMessage(const QString &msg);
void sendEmoteMessage(QString msg);
+ void clearRoomTimeline();
void heightChanged(int height);
void uploadMedia(const QSharedPointer<QIODevice> data,
diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
index 2248fb1a..f5dc49d8 100644
--- a/src/WebRTCSession.cpp
+++ b/src/WebRTCSession.cpp
@@ -223,18 +223,19 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
{
nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
+#if GST_CHECK_VERSION(1, 17, 0)
+ localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
+ return;
+#else
if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) {
emit WebRTCSession::instance().newICECandidate(
{"audio", (uint16_t)mlineIndex, candidate});
return;
}
- localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
-
// GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers
// GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early. Fixed in v1.17.
// Use a 100ms timeout in the meantime
-#if !GST_CHECK_VERSION(1, 17, 0)
static guint timerid = 0;
if (timerid)
g_source_remove(timerid);
@@ -282,11 +283,11 @@ linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe
GstElement *resample = gst_element_factory_make("audioresample", nullptr);
GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr);
gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
+ gst_element_link_many(queue, convert, resample, sink, nullptr);
gst_element_sync_state_with_parent(queue);
gst_element_sync_state_with_parent(convert);
gst_element_sync_state_with_parent(resample);
gst_element_sync_state_with_parent(sink);
- gst_element_link_many(queue, convert, resample, sink, nullptr);
queuepad = gst_element_get_static_pad(queue, "sink");
}
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index 26aece32..822b7218 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -151,7 +151,7 @@ EditModal::applyClicked()
state::Name body;
body.name = newName.toStdString();
- http::client()->send_state_event<state::Name, EventType::RoomName>(
+ http::client()->send_state_event(
roomId_.toStdString(),
body,
[proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
@@ -169,7 +169,7 @@ EditModal::applyClicked()
state::Topic body;
body.topic = newTopic.toStdString();
- http::client()->send_state_event<state::Topic, EventType::RoomTopic>(
+ http::client()->send_state_event(
roomId_.toStdString(),
body,
[proxy](const mtx::responses::EventId &, mtx::http::RequestErr err) {
@@ -694,7 +694,7 @@ RoomSettings::updateAccessRules(const std::string &room_id,
startLoadingSpinner();
resetErrorLabel();
- http::client()->send_state_event<state::JoinRules, EventType::RoomJoinRules>(
+ http::client()->send_state_event(
room_id,
join_rule,
[this, room_id, guest_access](const mtx::responses::EventId &,
@@ -708,7 +708,7 @@ RoomSettings::updateAccessRules(const std::string &room_id,
return;
}
- http::client()->send_state_event<state::GuestAccess, EventType::RoomGuestAccess>(
+ http::client()->send_state_event(
room_id,
guest_access,
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
@@ -843,7 +843,7 @@ RoomSettings::updateAvatar()
avatar_event.image_info.size = size;
avatar_event.url = res.content_uri;
- http::client()->send_state_event<state::Avatar, EventType::RoomAvatar>(
+ http::client()->send_state_event(
room_id,
avatar_event,
[content = std::move(content), proxy = std::move(proxy)](
diff --git a/src/main.cpp b/src/main.cpp
index 46691e6f..e02ffa36 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -173,11 +173,12 @@ main(int argc, char *argv[])
QString lang = QLocale::system().name();
QTranslator qtTranslator;
- qtTranslator.load("qt_" + lang, QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+ qtTranslator.load(
+ QLocale(), "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath));
app.installTranslator(&qtTranslator);
QTranslator appTranslator;
- appTranslator.load("nheko_" + lang, ":/translations");
+ appTranslator.load(QLocale(), "nheko", "_", ":/translations");
app.installTranslator(&appTranslator);
MainWindow w;
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
new file mode 100644
index 00000000..fca1d31d
--- /dev/null
+++ b/src/timeline/EventStore.cpp
@@ -0,0 +1,570 @@
+#include "EventStore.h"
+
+#include <QThread>
+#include <QTimer>
+
+#include "Cache.h"
+#include "Cache_p.h"
+#include "EventAccessors.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Olm.h"
+
+Q_DECLARE_METATYPE(Reaction)
+
+QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
+ 1000};
+QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
+ 1000};
+QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{1000};
+
+EventStore::EventStore(std::string room_id, QObject *)
+ : room_id_(std::move(room_id))
+{
+ static auto reactionType = qRegisterMetaType<Reaction>();
+ (void)reactionType;
+
+ auto range = cache::client()->getTimelineRange(room_id_);
+
+ if (range) {
+ this->first = range->first;
+ this->last = range->last;
+ }
+
+ connect(
+ this,
+ &EventStore::eventFetched,
+ this,
+ [this](std::string id,
+ std::string relatedTo,
+ mtx::events::collections::TimelineEvents timeline) {
+ cache::client()->storeEvent(room_id_, id, {timeline});
+
+ if (!relatedTo.empty()) {
+ auto idx = idToIndex(relatedTo);
+ if (idx)
+ emit dataChanged(*idx, *idx);
+ }
+ },
+ Qt::QueuedConnection);
+
+ connect(
+ this,
+ &EventStore::oldMessagesRetrieved,
+ this,
+ [this](const mtx::responses::Messages &res) {
+ //
+ uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
+ if (newFirst == first)
+ fetchMore();
+ else {
+ emit beginInsertRows(toExternalIdx(newFirst),
+ toExternalIdx(this->first - 1));
+ this->first = newFirst;
+ emit endInsertRows();
+ emit fetchedMore();
+ }
+ },
+ Qt::QueuedConnection);
+
+ connect(this, &EventStore::processPending, this, [this]() {
+ if (!current_txn.empty()) {
+ nhlog::ui()->debug("Already processing {}", current_txn);
+ return;
+ }
+
+ auto event = cache::client()->firstPendingMessage(room_id_);
+
+ if (!event) {
+ nhlog::ui()->debug("No event to send");
+ return;
+ }
+
+ std::visit(
+ [this](auto e) {
+ auto txn_id = e.event_id;
+ this->current_txn = txn_id;
+
+ if (txn_id.empty() || txn_id[0] != 'm') {
+ nhlog::ui()->debug("Invalid txn id '{}'", txn_id);
+ cache::client()->removePendingStatus(room_id_, txn_id);
+ return;
+ }
+
+ if constexpr (mtx::events::message_content_to_type<decltype(e.content)> !=
+ mtx::events::EventType::Unsupported)
+ http::client()->send_room_message(
+ room_id_,
+ txn_id,
+ e.content,
+ [this, txn_id](const mtx::responses::EventId &event_id,
+ mtx::http::RequestErr err) {
+ if (err) {
+ const int status_code =
+ static_cast<int>(err->status_code);
+ nhlog::net()->warn(
+ "[{}] failed to send message: {} {}",
+ txn_id,
+ err->matrix_error.error,
+ status_code);
+ emit messageFailed(txn_id);
+ return;
+ }
+ emit messageSent(txn_id, event_id.event_id.to_string());
+ });
+ },
+ event->data);
+ });
+
+ connect(
+ this,
+ &EventStore::messageFailed,
+ this,
+ [this](std::string txn_id) {
+ if (current_txn == txn_id) {
+ current_txn_error_count++;
+ if (current_txn_error_count > 10) {
+ nhlog::ui()->debug("failing txn id '{}'", txn_id);
+ cache::client()->removePendingStatus(room_id_, txn_id);
+ current_txn_error_count = 0;
+ }
+ }
+ QTimer::singleShot(1000, this, [this]() {
+ nhlog::ui()->debug("timeout");
+ this->current_txn = "";
+ emit processPending();
+ });
+ },
+ Qt::QueuedConnection);
+
+ connect(
+ this,
+ &EventStore::messageSent,
+ this,
+ [this](std::string txn_id, std::string event_id) {
+ nhlog::ui()->debug("sent {}", txn_id);
+
+ http::client()->read_event(
+ room_id_, event_id, [this, event_id](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn(
+ "failed to read_event ({}, {})", room_id_, event_id);
+ }
+ });
+
+ cache::client()->removePendingStatus(room_id_, txn_id);
+ this->current_txn = "";
+ this->current_txn_error_count = 0;
+ emit processPending();
+ },
+ Qt::QueuedConnection);
+}
+
+void
+EventStore::addPending(mtx::events::collections::TimelineEvents event)
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ cache::client()->savePendingMessage(this->room_id_, {event});
+ mtx::responses::Timeline events;
+ events.limited = false;
+ events.events.emplace_back(event);
+ handleSync(events);
+
+ emit processPending();
+}
+
+void
+EventStore::clearTimeline()
+{
+ emit beginResetModel();
+
+ cache::client()->clearTimeline(room_id_);
+ auto range = cache::client()->getTimelineRange(room_id_);
+ if (range) {
+ nhlog::db()->info("Range {} {}", range->last, range->first);
+ this->last = range->last;
+ this->first = range->first;
+ } else {
+ this->first = std::numeric_limits<uint64_t>::max();
+ this->last = std::numeric_limits<uint64_t>::max();
+ }
+ nhlog::ui()->info("Range {} {}", this->last, this->first);
+
+ emit endResetModel();
+}
+
+void
+EventStore::handleSync(const mtx::responses::Timeline &events)
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ auto range = cache::client()->getTimelineRange(room_id_);
+ if (!range)
+ return;
+
+ if (events.limited) {
+ emit beginResetModel();
+ this->last = range->last;
+ this->first = range->first;
+ emit endResetModel();
+
+ } else if (range->last > this->last) {
+ emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last));
+ this->last = range->last;
+ emit endInsertRows();
+ }
+
+ for (const auto &event : events.events) {
+ std::string relates_to;
+ if (auto redaction =
+ std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
+ &event)) {
+ // fixup reactions
+ auto redacted = events_by_id_.object({room_id_, redaction->redacts});
+ if (redacted) {
+ auto id = mtx::accessors::relates_to_event_id(*redacted);
+ if (!id.empty()) {
+ auto idx = idToIndex(id);
+ if (idx) {
+ events_by_id_.remove(
+ {room_id_, redaction->redacts});
+ events_.remove({room_id_, toInternalIdx(*idx)});
+ emit dataChanged(*idx, *idx);
+ }
+ }
+ }
+
+ relates_to = redaction->redacts;
+ } else if (auto reaction =
+ std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
+ &event)) {
+ relates_to = reaction->content.relates_to.event_id;
+ } else {
+ relates_to = mtx::accessors::in_reply_to_event(event);
+ }
+
+ if (!relates_to.empty()) {
+ auto idx = cache::client()->getTimelineIndex(room_id_, relates_to);
+ if (idx) {
+ events_by_id_.remove({room_id_, relates_to});
+ decryptedEvents_.remove({room_id_, relates_to});
+ events_.remove({room_id_, *idx});
+ emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
+ }
+ }
+
+ if (auto txn_id = mtx::accessors::transaction_id(event); !txn_id.empty()) {
+ auto idx = cache::client()->getTimelineIndex(
+ room_id_, mtx::accessors::event_id(event));
+ if (idx) {
+ Index index{room_id_, *idx};
+ events_.remove(index);
+ emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
+ }
+ }
+ }
+}
+
+QVariantList
+EventStore::reactions(const std::string &event_id)
+{
+ auto event_ids = cache::client()->relatedEvents(room_id_, event_id);
+
+ struct TempReaction
+ {
+ int count = 0;
+ std::vector<std::string> users;
+ std::string reactedBySelf;
+ };
+ std::map<std::string, TempReaction> aggregation;
+ std::vector<Reaction> reactions;
+
+ auto self = http::client()->user_id().to_string();
+ for (const auto &id : event_ids) {
+ auto related_event = get(id, event_id);
+ if (!related_event)
+ continue;
+
+ if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
+ related_event)) {
+ auto &agg = aggregation[reaction->content.relates_to.key];
+
+ if (agg.count == 0) {
+ Reaction temp{};
+ temp.key_ =
+ QString::fromStdString(reaction->content.relates_to.key);
+ reactions.push_back(temp);
+ }
+
+ agg.count++;
+ agg.users.push_back(cache::displayName(room_id_, reaction->sender));
+ if (reaction->sender == self)
+ agg.reactedBySelf = reaction->event_id;
+ }
+ }
+
+ QVariantList temp;
+ for (auto &reaction : reactions) {
+ const auto &agg = aggregation[reaction.key_.toStdString()];
+ reaction.count_ = agg.count;
+ reaction.selfReactedEvent_ = QString::fromStdString(agg.reactedBySelf);
+
+ bool firstReaction = true;
+ for (const auto &user : agg.users) {
+ if (firstReaction)
+ firstReaction = false;
+ else
+ reaction.users_ += ", ";
+
+ reaction.users_ += QString::fromStdString(user);
+ }
+
+ nhlog::db()->debug("key: {}, count: {}, users: {}",
+ reaction.key_.toStdString(),
+ reaction.count_,
+ reaction.users_.toStdString());
+ temp.append(QVariant::fromValue(reaction));
+ }
+
+ return temp;
+}
+
+mtx::events::collections::TimelineEvents *
+EventStore::get(int idx, bool decrypt)
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ Index index{room_id_, toInternalIdx(idx)};
+ if (index.idx > last || index.idx < first)
+ return nullptr;
+
+ auto event_ptr = events_.object(index);
+ if (!event_ptr) {
+ auto event_id = cache::client()->getTimelineEventId(room_id_, index.idx);
+ if (!event_id)
+ return nullptr;
+
+ auto event = cache::client()->getEvent(room_id_, *event_id);
+ if (!event)
+ return nullptr;
+ else
+ event_ptr =
+ new mtx::events::collections::TimelineEvents(std::move(event->data));
+ events_.insert(index, event_ptr);
+ }
+
+ if (decrypt)
+ if (auto encrypted =
+ std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+ event_ptr))
+ return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+
+ return event_ptr;
+}
+
+std::optional<int>
+EventStore::idToIndex(std::string_view id) const
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ auto idx = cache::client()->getTimelineIndex(room_id_, id);
+ if (idx)
+ return toExternalIdx(*idx);
+ else
+ return std::nullopt;
+}
+std::optional<std::string>
+EventStore::indexToId(int idx) const
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
+}
+
+mtx::events::collections::TimelineEvents *
+EventStore::decryptEvent(const IdIndex &idx,
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
+{
+ if (auto cachedEvent = decryptedEvents_.object(idx))
+ return cachedEvent;
+
+ MegolmSessionIndex index;
+ index.room_id = room_id_;
+ index.session_id = e.content.session_id;
+ index.sender_key = e.content.sender_key;
+
+ auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
+ auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
+ decryptedEvents_.insert(idx, event_ptr);
+ return event_ptr;
+ };
+
+ auto decryptionResult = olm::decryptEvent(index, e);
+
+ if (decryptionResult.error) {
+ mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
+ dummy.origin_server_ts = e.origin_server_ts;
+ dummy.event_id = e.event_id;
+ dummy.sender = e.sender;
+ switch (*decryptionResult.error) {
+ case olm::DecryptionErrorCode::MissingSession:
+ dummy.content.body =
+ tr("-- Encrypted Event (No keys found for decryption) --",
+ "Placeholder, when the message was not decrypted yet or can't be "
+ "decrypted.")
+ .toStdString();
+ nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+ index.room_id,
+ index.session_id,
+ e.sender);
+ // TODO: Check if this actually works and look in key backup
+ olm::send_key_request_for(room_id_, e);
+ break;
+ case olm::DecryptionErrorCode::DbError:
+ nhlog::db()->critical(
+ "failed to retrieve megolm session with index ({}, {}, {})",
+ index.room_id,
+ index.session_id,
+ index.sender_key,
+ decryptionResult.error_message.value_or(""));
+ dummy.content.body =
+ tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
+ "Placeholder, when the message can't be decrypted, because the DB "
+ "access "
+ "failed.")
+ .toStdString();
+ break;
+ case olm::DecryptionErrorCode::DecryptionFailed:
+ nhlog::crypto()->critical(
+ "failed to decrypt message with index ({}, {}, {}): {}",
+ index.room_id,
+ index.session_id,
+ index.sender_key,
+ decryptionResult.error_message.value_or(""));
+ dummy.content.body =
+ tr("-- Decryption Error (%1) --",
+ "Placeholder, when the message can't be decrypted. In this case, the "
+ "Olm "
+ "decrytion returned an error, which is passed as %1.")
+ .arg(
+ QString::fromStdString(decryptionResult.error_message.value_or("")))
+ .toStdString();
+ break;
+ case olm::DecryptionErrorCode::ParsingFailed:
+ dummy.content.body =
+ tr("-- Encrypted Event (Unknown event type) --",
+ "Placeholder, when the message was decrypted, but we couldn't parse "
+ "it, because "
+ "Nheko/mtxclient don't support that event type yet.")
+ .toStdString();
+ break;
+ case olm::DecryptionErrorCode::ReplayAttack:
+ nhlog::crypto()->critical(
+ "Reply attack while decryptiong event {} in room {} from {}!",
+ e.event_id,
+ room_id_,
+ index.sender_key);
+ dummy.content.body =
+ tr("-- Reply attack! This message index was reused! --").toStdString();
+ break;
+ case olm::DecryptionErrorCode::UnknownFingerprint:
+ // TODO: don't fail, just show in UI.
+ nhlog::crypto()->critical("Message by unverified fingerprint {}",
+ index.sender_key);
+ dummy.content.body =
+ tr("-- Message by unverified device! --").toStdString();
+ break;
+ }
+ return asCacheEntry(std::move(dummy));
+ }
+
+ auto encInfo = mtx::accessors::file(decryptionResult.event.value());
+ if (encInfo)
+ emit newEncryptedImage(encInfo.value());
+
+ return asCacheEntry(std::move(decryptionResult.event.value()));
+}
+
+mtx::events::collections::TimelineEvents *
+EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ if (id.empty())
+ return nullptr;
+
+ IdIndex index{room_id_, std::string(id.data(), id.size())};
+
+ auto event_ptr = events_by_id_.object(index);
+ if (!event_ptr) {
+ auto event = cache::client()->getEvent(room_id_, index.id);
+ if (!event) {
+ http::client()->get_event(
+ room_id_,
+ index.id,
+ [this,
+ relatedTo = std::string(related_to.data(), related_to.size()),
+ id = index.id](const mtx::events::collections::TimelineEvents &timeline,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error(
+ "Failed to retrieve event with id {}, which was "
+ "requested to show the replyTo for event {}",
+ relatedTo,
+ id);
+ return;
+ }
+ emit eventFetched(id, relatedTo, timeline);
+ });
+ return nullptr;
+ }
+ event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
+ events_by_id_.insert(index, event_ptr);
+ }
+
+ if (decrypt)
+ if (auto encrypted =
+ std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+ event_ptr))
+ return decryptEvent(index, *encrypted);
+
+ return event_ptr;
+}
+
+void
+EventStore::fetchMore()
+{
+ mtx::http::MessagesOpts opts;
+ opts.room_id = room_id_;
+ opts.from = cache::client()->previousBatchToken(room_id_);
+
+ nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from);
+
+ http::client()->messages(
+ opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
+ if (cache::client()->previousBatchToken(room_id_) != opts.from) {
+ nhlog::net()->warn("Cache cleared while fetching more messages, dropping "
+ "/messages response");
+ emit fetchedMore();
+ return;
+ }
+ if (err) {
+ nhlog::net()->error("failed to call /messages ({}): {} - {} - {}",
+ opts.room_id,
+ mtx::errors::to_string(err->matrix_error.errcode),
+ err->matrix_error.error,
+ err->parse_error);
+ emit fetchedMore();
+ return;
+ }
+
+ emit oldMessagesRetrieved(std::move(res));
+ });
+}
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
new file mode 100644
index 00000000..d4353a18
--- /dev/null
+++ b/src/timeline/EventStore.h
@@ -0,0 +1,122 @@
+#pragma once
+
+#include <limits>
+#include <string>
+
+#include <QCache>
+#include <QObject>
+#include <QVariant>
+#include <qhashfunctions.h>
+
+#include <mtx/events/collections.hpp>
+#include <mtx/responses/messages.hpp>
+#include <mtx/responses/sync.hpp>
+
+#include "Reaction.h"
+
+class EventStore : public QObject
+{
+ Q_OBJECT
+
+public:
+ EventStore(std::string room_id, QObject *parent);
+
+ struct Index
+ {
+ std::string room;
+ uint64_t idx;
+
+ friend uint qHash(const Index &i, uint seed = 0) noexcept
+ {
+ QtPrivate::QHashCombine hash;
+ seed = hash(seed, QByteArray::fromRawData(i.room.data(), i.room.size()));
+ seed = hash(seed, i.idx);
+ return seed;
+ }
+
+ friend bool operator==(const Index &a, const Index &b) noexcept
+ {
+ return a.idx == b.idx && a.room == b.room;
+ }
+ };
+ struct IdIndex
+ {
+ std::string room, id;
+
+ friend uint qHash(const IdIndex &i, uint seed = 0) noexcept
+ {
+ QtPrivate::QHashCombine hash;
+ seed = hash(seed, QByteArray::fromRawData(i.room.data(), i.room.size()));
+ seed = hash(seed, QByteArray::fromRawData(i.id.data(), i.id.size()));
+ return seed;
+ }
+
+ friend bool operator==(const IdIndex &a, const IdIndex &b) noexcept
+ {
+ return a.id == b.id && a.room == b.room;
+ }
+ };
+
+ void fetchMore();
+ void handleSync(const mtx::responses::Timeline &events);
+
+ // optionally returns the event or nullptr and fetches it, after which it emits a
+ // relatedFetched event
+ mtx::events::collections::TimelineEvents *get(std::string_view id,
+ std::string_view related_to,
+ bool decrypt = true);
+ // always returns a proper event as long as the idx is valid
+ mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
+
+ QVariantList reactions(const std::string &event_id);
+
+ int size() const
+ {
+ return last != std::numeric_limits<uint64_t>::max()
+ ? static_cast<int>(last - first) + 1
+ : 0;
+ }
+ int toExternalIdx(uint64_t idx) const { return static_cast<int>(idx - first); }
+ uint64_t toInternalIdx(int idx) const { return first + idx; }
+
+ std::optional<int> idToIndex(std::string_view id) const;
+ std::optional<std::string> indexToId(int idx) const;
+
+signals:
+ void beginInsertRows(int from, int to);
+ void endInsertRows();
+ void beginResetModel();
+ void endResetModel();
+ void dataChanged(int from, int to);
+ void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
+ void eventFetched(std::string id,
+ std::string relatedTo,
+ mtx::events::collections::TimelineEvents timeline);
+ void oldMessagesRetrieved(const mtx::responses::Messages &);
+ void fetchedMore();
+
+ void processPending();
+ void messageSent(std::string txn_id, std::string event_id);
+ void messageFailed(std::string txn_id);
+
+public slots:
+ void addPending(mtx::events::collections::TimelineEvents event);
+ void clearTimeline();
+
+private:
+ mtx::events::collections::TimelineEvents *decryptEvent(
+ const IdIndex &idx,
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
+
+ std::string room_id_;
+
+ uint64_t first = std::numeric_limits<uint64_t>::max(),
+ last = std::numeric_limits<uint64_t>::max();
+
+ static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_;
+ static QCache<Index, mtx::events::collections::TimelineEvents> events_;
+ static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
+
+ std::string current_txn;
+ int current_txn_error_count = 0;
+};
diff --git a/src/timeline/Reaction.cpp b/src/timeline/Reaction.cpp
new file mode 100644
index 00000000..343c4649
--- /dev/null
+++ b/src/timeline/Reaction.cpp
@@ -0,0 +1 @@
+#include "Reaction.h"
diff --git a/src/timeline/Reaction.h b/src/timeline/Reaction.h
new file mode 100644
index 00000000..5f122e0a
--- /dev/null
+++ b/src/timeline/Reaction.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <QObject>
+#include <QString>
+
+struct Reaction
+{
+ Q_GADGET
+ Q_PROPERTY(QString key READ key)
+ Q_PROPERTY(QString users READ users)
+ Q_PROPERTY(QString selfReactedEvent READ selfReactedEvent)
+ Q_PROPERTY(int count READ count)
+
+public:
+ QString key() const { return key_; }
+ QString users() const { return users_; }
+ QString selfReactedEvent() const { return selfReactedEvent_; }
+ int count() const { return count_; }
+
+ QString key_;
+ QString users_;
+ QString selfReactedEvent_;
+ int count_;
+};
diff --git a/src/timeline/ReactionsModel.cpp b/src/timeline/ReactionsModel.cpp
deleted file mode 100644
index 1200e2ba..00000000
--- a/src/timeline/ReactionsModel.cpp
+++ /dev/null
@@ -1,98 +0,0 @@
-#include "ReactionsModel.h"
-
-#include <Cache.h>
-#include <MatrixClient.h>
-
-QHash<int, QByteArray>
-ReactionsModel::roleNames() const
-{
- return {
- {Key, "key"},
- {Count, "counter"},
- {Users, "users"},
- {SelfReactedEvent, "selfReactedEvent"},
- };
-}
-
-int
-ReactionsModel::rowCount(const QModelIndex &) const
-{
- return static_cast<int>(reactions.size());
-}
-
-QVariant
-ReactionsModel::data(const QModelIndex &index, int role) const
-{
- const int i = index.row();
- if (i < 0 || i >= static_cast<int>(reactions.size()))
- return {};
-
- switch (role) {
- case Key:
- return QString::fromStdString(reactions[i].key);
- case Count:
- return static_cast<int>(reactions[i].reactions.size());
- case Users: {
- QString users;
- bool first = true;
- for (const auto &reaction : reactions[i].reactions) {
- if (!first)
- users += ", ";
- else
- first = false;
- users += QString::fromStdString(
- cache::displayName(room_id_, reaction.second.sender));
- }
- return users;
- }
- case SelfReactedEvent:
- for (const auto &reaction : reactions[i].reactions)
- if (reaction.second.sender == http::client()->user_id().to_string())
- return QString::fromStdString(reaction.second.event_id);
- return QStringLiteral("");
- default:
- return {};
- }
-}
-
-void
-ReactionsModel::addReaction(const std::string &room_id,
- const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction)
-{
- room_id_ = room_id;
-
- int idx = 0;
- for (auto &storedReactions : reactions) {
- if (storedReactions.key == reaction.content.relates_to.key) {
- storedReactions.reactions[reaction.event_id] = reaction;
- emit dataChanged(index(idx, 0), index(idx, 0));
- return;
- }
- idx++;
- }
-
- beginInsertRows(QModelIndex(), idx, idx);
- reactions.push_back(
- KeyReaction{reaction.content.relates_to.key, {{reaction.event_id, reaction}}});
- endInsertRows();
-}
-
-void
-ReactionsModel::removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction)
-{
- int idx = 0;
- for (auto &storedReactions : reactions) {
- if (storedReactions.key == reaction.content.relates_to.key) {
- storedReactions.reactions.erase(reaction.event_id);
-
- if (storedReactions.reactions.size() == 0) {
- beginRemoveRows(QModelIndex(), idx, idx);
- reactions.erase(reactions.begin() + idx);
- endRemoveRows();
- } else
- emit dataChanged(index(idx, 0), index(idx, 0));
- return;
- }
- idx++;
- }
-}
diff --git a/src/timeline/ReactionsModel.h b/src/timeline/ReactionsModel.h
deleted file mode 100644
index c839afc8..00000000
--- a/src/timeline/ReactionsModel.h
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma once
-
-#include <QAbstractListModel>
-#include <QHash>
-
-#include <utility>
-#include <vector>
-
-#include <mtx/events/collections.hpp>
-
-class ReactionsModel : public QAbstractListModel
-{
- Q_OBJECT
-public:
- explicit ReactionsModel(QObject *parent = nullptr) { Q_UNUSED(parent); }
- enum Roles
- {
- Key,
- Count,
- Users,
- SelfReactedEvent,
- };
-
- QHash<int, QByteArray> roleNames() const override;
- int rowCount(const QModelIndex &parent = QModelIndex()) const override;
- QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
-
-public slots:
- void addReaction(const std::string &room_id,
- const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction);
- void removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction);
-
-private:
- struct KeyReaction
- {
- std::string key;
- std::map<std::string, mtx::events::RoomEvent<mtx::events::msg::Reaction>> reactions;
- };
- std::string room_id_;
- std::vector<KeyReaction> reactions;
-};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 67e07d7b..b6c2d4bb 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -136,6 +136,11 @@ struct RoomEventType
{
return qml_mtx_events::EventType::CallHangUp;
}
+ qml_mtx_events::EventType operator()(
+ const mtx::events::Event<mtx::events::msg::CallCandidates> &)
+ {
+ return qml_mtx_events::EventType::CallCandidates;
+ }
// ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
// ::EventType::LocationMessage; }
};
@@ -156,70 +161,10 @@ toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
: QAbstractListModel(parent)
+ , events(room_id.toStdString(), this)
, room_id_(room_id)
, manager_(manager)
{
- connect(this,
- &TimelineModel::oldMessagesRetrieved,
- this,
- &TimelineModel::addBackwardsEvents,
- Qt::QueuedConnection);
- connect(
- this,
- &TimelineModel::messageFailed,
- this,
- [this](QString txn_id) {
- nhlog::ui()->error("Failed to send {}, retrying", txn_id.toStdString());
-
- QTimer::singleShot(5000, this, [this]() { emit nextPendingMessage(); });
- },
- Qt::QueuedConnection);
- connect(
- this,
- &TimelineModel::messageSent,
- this,
- [this](QString txn_id, QString event_id) {
- pending.removeOne(txn_id);
-
- auto ev = events.value(txn_id);
-
- if (auto reaction =
- std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&ev)) {
- QString reactedTo =
- QString::fromStdString(reaction->content.relates_to.event_id);
- auto &rModel = reactions[reactedTo];
- rModel.removeReaction(*reaction);
- auto rCopy = *reaction;
- rCopy.event_id = event_id.toStdString();
- rModel.addReaction(room_id_.toStdString(), rCopy);
- }
-
- int idx = idToIndex(txn_id);
- if (idx < 0) {
- // transaction already received via sync
- return;
- }
- eventOrder[idx] = event_id;
- ev = std::visit(
- [event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
- auto eventCopy = e;
- eventCopy.event_id = event_id.toStdString();
- return eventCopy;
- },
- ev);
-
- events.remove(txn_id);
- events.insert(event_id, ev);
-
- // mark our messages as read
- readEvent(event_id.toStdString());
-
- emit dataChanged(index(idx, 0), index(idx, 0));
-
- if (pending.size() > 0)
- emit nextPendingMessage();
- },
- Qt::QueuedConnection);
connect(
this,
&TimelineModel::redactionFailed,
@@ -228,27 +173,44 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
Qt::QueuedConnection);
connect(this,
- &TimelineModel::nextPendingMessage,
- this,
- &TimelineModel::processOnePendingMessage,
- Qt::QueuedConnection);
- connect(this,
&TimelineModel::newMessageToSend,
this,
&TimelineModel::addPendingMessage,
Qt::QueuedConnection);
+ connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending);
connect(
+ &events,
+ &EventStore::dataChanged,
this,
- &TimelineModel::eventFetched,
- this,
- [this](QString requestingEvent, mtx::events::collections::TimelineEvents event) {
- events.insert(QString::fromStdString(mtx::accessors::event_id(event)), event);
- auto idx = idToIndex(requestingEvent);
- if (idx >= 0)
- emit dataChanged(index(idx, 0), index(idx, 0));
+ [this](int from, int to) {
+ nhlog::ui()->debug(
+ "data changed {} to {}", events.size() - to - 1, events.size() - from - 1);
+ emit dataChanged(index(events.size() - to - 1, 0),
+ index(events.size() - from - 1, 0));
},
Qt::QueuedConnection);
+
+ connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) {
+ int first = events.size() - to;
+ int last = events.size() - from;
+ if (from >= events.size()) {
+ int batch_size = to - from;
+ first += batch_size;
+ last += batch_size;
+ } else {
+ first -= 1;
+ last -= 1;
+ }
+ nhlog::ui()->debug("begin insert from {} to {}", first, last);
+ beginInsertRows(QModelIndex(), first, last);
+ });
+ connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); });
+ connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); });
+ connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); });
+ connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
+ connect(
+ &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
}
QHash<int, QByteArray>
@@ -290,28 +252,22 @@ int
TimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
- return (int)this->eventOrder.size();
+ return this->events.size();
}
QVariantMap
-TimelineModel::getDump(QString eventId) const
+TimelineModel::getDump(QString eventId, QString relatedTo) const
{
- if (events.contains(eventId))
- return data(eventId, Dump).toMap();
+ if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString()))
+ return data(*event, Dump).toMap();
return {};
}
QVariant
-TimelineModel::data(const QString &id, int role) const
+TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const
{
using namespace mtx::accessors;
- namespace acc = mtx::accessors;
- mtx::events::collections::TimelineEvents event = events.value(id);
-
- if (auto e =
- std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
- event = decryptEvent(*e).event;
- }
+ namespace acc = mtx::accessors;
switch (role) {
case UserId:
@@ -397,8 +353,9 @@ TimelineModel::data(const QString &id, int role) const
return QVariant(prop > 0 ? prop : 1.);
}
case Id:
- return id;
+ return QVariant(QString::fromStdString(event_id(event)));
case State: {
+ auto id = QString::fromStdString(event_id(event));
auto containsOthers = [](const auto &vec) {
for (const auto &e : vec)
if (e.second != http::client()->user_id().to_string())
@@ -409,7 +366,7 @@ TimelineModel::data(const QString &id, int role) const
// only show read receipts for messages not from us
if (acc::sender(event) != http::client()->user_id().to_string())
return qml_mtx_events::Empty;
- else if (pending.contains(id))
+ else if (!id.isEmpty() && id[0] == "m")
return qml_mtx_events::Sent;
else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_)))
return qml_mtx_events::Read;
@@ -417,19 +374,22 @@ TimelineModel::data(const QString &id, int role) const
return qml_mtx_events::Received;
}
case IsEncrypted: {
- return std::holds_alternative<
- mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]);
+ auto id = event_id(event);
+ auto encrypted_event = events.get(id, id, false);
+ return encrypted_event &&
+ std::holds_alternative<
+ mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+ *encrypted_event);
}
case IsRoomEncrypted: {
return cache::isRoomEncrypted(room_id_.toStdString());
}
case ReplyTo:
return QVariant(QString::fromStdString(in_reply_to_event(event)));
- case Reactions:
- if (reactions.count(id))
- return QVariant::fromValue((QObject *)&reactions.at(id));
- else
- return {};
+ case Reactions: {
+ auto id = event_id(event);
+ return QVariant::fromValue(events.reactions(id));
+ }
case RoomId:
return QVariant(room_id_);
case RoomName:
@@ -443,31 +403,32 @@ TimelineModel::data(const QString &id, int role) const
auto names = roleNames();
// m.insert(names[Section], data(id, static_cast<int>(Section)));
- m.insert(names[Type], data(id, static_cast<int>(Type)));
- m.insert(names[TypeString], data(id, static_cast<int>(TypeString)));
- m.insert(names[IsOnlyEmoji], data(id, static_cast<int>(IsOnlyEmoji)));
- m.insert(names[Body], data(id, static_cast<int>(Body)));
- m.insert(names[FormattedBody], data(id, static_cast<int>(FormattedBody)));
- m.insert(names[UserId], data(id, static_cast<int>(UserId)));
- m.insert(names[UserName], data(id, static_cast<int>(UserName)));
- m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp)));
- m.insert(names[Url], data(id, static_cast<int>(Url)));
- m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl)));
- m.insert(names[Blurhash], data(id, static_cast<int>(Blurhash)));
- m.insert(names[Filename], data(id, static_cast<int>(Filename)));
- m.insert(names[Filesize], data(id, static_cast<int>(Filesize)));
- m.insert(names[MimeType], data(id, static_cast<int>(MimeType)));
- m.insert(names[Height], data(id, static_cast<int>(Height)));
- m.insert(names[Width], data(id, static_cast<int>(Width)));
- m.insert(names[ProportionalHeight], data(id, static_cast<int>(ProportionalHeight)));
- m.insert(names[Id], data(id, static_cast<int>(Id)));
- m.insert(names[State], data(id, static_cast<int>(State)));
- m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted)));
- m.insert(names[IsRoomEncrypted], data(id, static_cast<int>(IsRoomEncrypted)));
- m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo)));
- m.insert(names[RoomName], data(id, static_cast<int>(RoomName)));
- m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic)));
- m.insert(names[CallType], data(id, static_cast<int>(CallType)));
+ m.insert(names[Type], data(event, static_cast<int>(Type)));
+ m.insert(names[TypeString], data(event, static_cast<int>(TypeString)));
+ m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji)));
+ m.insert(names[Body], data(event, static_cast<int>(Body)));
+ m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody)));
+ m.insert(names[UserId], data(event, static_cast<int>(UserId)));
+ m.insert(names[UserName], data(event, static_cast<int>(UserName)));
+ m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp)));
+ m.insert(names[Url], data(event, static_cast<int>(Url)));
+ m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl)));
+ m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash)));
+ m.insert(names[Filename], data(event, static_cast<int>(Filename)));
+ m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
+ m.insert(names[MimeType], data(event, static_cast<int>(MimeType)));
+ m.insert(names[Height], data(event, static_cast<int>(Height)));
+ m.insert(names[Width], data(event, static_cast<int>(Width)));
+ m.insert(names[ProportionalHeight],
+ data(event, static_cast<int>(ProportionalHeight)));
+ m.insert(names[Id], data(event, static_cast<int>(Id)));
+ m.insert(names[State], data(event, static_cast<int>(State)));
+ m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
+ m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
+ m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
+ m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
+ m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
+ m.insert(names[CallType], data(event, static_cast<int>(CallType)));
return QVariant(m);
}
@@ -481,29 +442,33 @@ TimelineModel::data(const QModelIndex &index, int role) const
{
using namespace mtx::accessors;
namespace acc = mtx::accessors;
- if (index.row() < 0 && index.row() >= (int)eventOrder.size())
+ if (index.row() < 0 && index.row() >= rowCount())
return QVariant();
- QString id = eventOrder[index.row()];
+ auto event = events.get(rowCount() - index.row() - 1);
- mtx::events::collections::TimelineEvents event = events.value(id);
+ if (!event)
+ return "";
if (role == Section) {
- QDateTime date = origin_server_ts(event);
+ QDateTime date = origin_server_ts(*event);
date.setTime(QTime());
- std::string userId = acc::sender(event);
+ std::string userId = acc::sender(*event);
+
+ for (int r = rowCount() - index.row(); r < events.size(); r++) {
+ auto tempEv = events.get(r);
+ if (!tempEv)
+ break;
- for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
- auto tempEv = events.value(eventOrder[r]);
- QDateTime prevDate = origin_server_ts(tempEv);
+ QDateTime prevDate = origin_server_ts(*tempEv);
prevDate.setTime(QTime());
if (prevDate != date)
return QString("%2 %1")
.arg(date.toMSecsSinceEpoch())
.arg(QString::fromStdString(userId));
- std::string prevUserId = acc::sender(tempEv);
+ std::string prevUserId = acc::sender(*tempEv);
if (userId != prevUserId)
break;
}
@@ -511,16 +476,17 @@ TimelineModel::data(const QModelIndex &index, int role) const
return QString("%1").arg(QString::fromStdString(userId));
}
- return data(id, role);
+ return data(*event, role);
}
bool
TimelineModel::canFetchMore(const QModelIndex &) const
{
- if (eventOrder.empty())
+ if (!events.size())
return true;
- if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(
- events[eventOrder.back()]))
+ if (auto first = events.get(0);
+ first &&
+ !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first))
return true;
else
@@ -547,50 +513,44 @@ TimelineModel::fetchMore(const QModelIndex &)
}
setPaginationInProgress(true);
- mtx::http::MessagesOpts opts;
- opts.room_id = room_id_.toStdString();
- opts.from = prev_batch_token_.toStdString();
- nhlog::ui()->debug("Paginating room {}", opts.room_id);
-
- http::client()->messages(
- opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->error("failed to call /messages ({}): {} - {} - {}",
- opts.room_id,
- mtx::errors::to_string(err->matrix_error.errcode),
- err->matrix_error.error,
- err->parse_error);
- setPaginationInProgress(false);
- return;
- }
-
- emit oldMessagesRetrieved(std::move(res));
- setPaginationInProgress(false);
- });
+ events.fetchMore();
}
void
TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
{
- if (isInitialSync) {
- prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
- isInitialSync = false;
- }
-
if (timeline.events.empty())
return;
- std::vector<QString> ids = internalAddEvents(timeline.events, true);
+ events.handleSync(timeline);
- if (!ids.empty()) {
- beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1));
- this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
- endInsertRows();
- }
+ using namespace mtx::events;
+ for (auto e : timeline.events) {
+ if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
+ MegolmSessionIndex index;
+ index.room_id = room_id_.toStdString();
+ index.session_id = encryptedEvent->content.session_id;
+ index.sender_key = encryptedEvent->content.sender_key;
+
+ auto result = olm::decryptEvent(index, *encryptedEvent);
+ if (result.event)
+ e = result.event.value();
+ }
- if (!timeline.events.empty())
- updateLastMessage();
+ if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) ||
+ std::holds_alternative<RoomEvent<msg::CallInvite>>(e) ||
+ std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) ||
+ std::holds_alternative<RoomEvent<msg::CallHangUp>>(e))
+ std::visit(
+ [this](auto &event) {
+ event.room_id = room_id_.toStdString();
+ if (event.sender != http::client()->user_id().to_string())
+ emit newCallEvent(event);
+ },
+ e);
+ }
+ updateLastMessage();
}
template<typename T>
@@ -649,21 +609,17 @@ isYourJoin(const mtx::events::Event<T> &)
void
TimelineModel::updateLastMessage()
{
- for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) {
- auto event = events.value(*it);
- if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
- &event)) {
- if (decryptDescription) {
- event = decryptEvent(*e).event;
- }
- }
+ for (auto it = events.size() - 1; it >= 0; --it) {
+ auto event = events.get(it, decryptDescription);
+ if (!event)
+ continue;
- if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, event)) {
- auto time = mtx::accessors::origin_server_ts(event);
+ if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
+ auto time = mtx::accessors::origin_server_ts(*event);
uint64_t ts = time.toMSecsSinceEpoch();
emit manager_->updateRoomsLastMessage(
room_id_,
- DescInfo{QString::fromStdString(mtx::accessors::event_id(event)),
+ DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
QString::fromStdString(http::client()->user_id().to_string()),
tr("You joined this room."),
utils::descriptiveTime(time),
@@ -671,191 +627,16 @@ TimelineModel::updateLastMessage()
time});
return;
}
- if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, event))
+ if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
continue;
auto description = utils::getMessageDescription(
- event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
+ *event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
emit manager_->updateRoomsLastMessage(room_id_, description);
return;
}
}
-std::vector<QString>
-TimelineModel::internalAddEvents(
- const std::vector<mtx::events::collections::TimelineEvents> &timeline,
- bool emitCallEvents)
-{
- std::vector<QString> ids;
- for (auto e : timeline) {
- QString id = QString::fromStdString(mtx::accessors::event_id(e));
-
- if (this->events.contains(id)) {
- this->events.insert(id, e);
- int idx = idToIndex(id);
- emit dataChanged(index(idx, 0), index(idx, 0));
- continue;
- }
-
- QString txid = QString::fromStdString(mtx::accessors::transaction_id(e));
- if (this->pending.removeOne(txid)) {
- this->events.insert(id, e);
- this->events.remove(txid);
- int idx = idToIndex(txid);
- if (idx < 0) {
- nhlog::ui()->warn("Received index out of range");
- continue;
- }
- eventOrder[idx] = id;
- emit dataChanged(index(idx, 0), index(idx, 0));
- continue;
- }
-
- if (auto redaction =
- std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
- QString redacts = QString::fromStdString(redaction->redacts);
- auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts);
-
- auto event = events.value(redacts);
- if (auto reaction =
- std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
- &event)) {
- QString reactedTo =
- QString::fromStdString(reaction->content.relates_to.event_id);
- reactions[reactedTo].removeReaction(*reaction);
- int idx = idToIndex(reactedTo);
- if (idx >= 0)
- emit dataChanged(index(idx, 0), index(idx, 0));
- }
-
- if (redacted != eventOrder.end()) {
- auto redactedEvent = std::visit(
- [](const auto &ev)
- -> mtx::events::RoomEvent<mtx::events::msg::Redacted> {
- mtx::events::RoomEvent<mtx::events::msg::Redacted>
- replacement = {};
- replacement.event_id = ev.event_id;
- replacement.room_id = ev.room_id;
- replacement.sender = ev.sender;
- replacement.origin_server_ts = ev.origin_server_ts;
- replacement.type = ev.type;
- return replacement;
- },
- e);
- events.insert(redacts, redactedEvent);
-
- int row = (int)std::distance(eventOrder.begin(), redacted);
- emit dataChanged(index(row, 0), index(row, 0));
- }
-
- continue; // don't insert redaction into timeline
- }
-
- if (auto reaction =
- std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&e)) {
- QString reactedTo =
- QString::fromStdString(reaction->content.relates_to.event_id);
- events.insert(id, e);
-
- // remove local echo
- if (!txid.isEmpty()) {
- auto rCopy = *reaction;
- rCopy.event_id = txid.toStdString();
- reactions[reactedTo].removeReaction(rCopy);
- }
-
- reactions[reactedTo].addReaction(room_id_.toStdString(), *reaction);
- int idx = idToIndex(reactedTo);
- if (idx >= 0)
- emit dataChanged(index(idx, 0), index(idx, 0));
- continue; // don't insert reaction into timeline
- }
-
- if (auto event =
- std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
- auto e_ = decryptEvent(*event).event;
- auto encInfo = mtx::accessors::file(e_);
-
- if (encInfo)
- emit newEncryptedImage(encInfo.value());
-
- if (std::holds_alternative<
- mtx::events::RoomEvent<mtx::events::msg::CallCandidates>>(e_)) {
- // don't display CallCandidate events to user
- events.insert(id, e);
- if (emitCallEvents)
- emit newCallEvent(e_);
- continue;
- }
-
- if (emitCallEvents) {
- if (auto callInvite = std::get_if<
- mtx::events::RoomEvent<mtx::events::msg::CallInvite>>(&e_)) {
- callInvite->room_id = room_id_.toStdString();
- emit newCallEvent(e_);
- } else if (std::holds_alternative<mtx::events::RoomEvent<
- mtx::events::msg::CallCandidates>>(e_) ||
- std::holds_alternative<
- mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>(
- e_) ||
- std::holds_alternative<
- mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>(
- e_)) {
- emit newCallEvent(e_);
- }
- }
- }
-
- if (std::holds_alternative<
- mtx::events::RoomEvent<mtx::events::msg::CallCandidates>>(e)) {
- // don't display CallCandidate events to user
- events.insert(id, e);
- if (emitCallEvents)
- emit newCallEvent(e);
- continue;
- }
-
- if (emitCallEvents) {
- if (auto callInvite =
- std::get_if<mtx::events::RoomEvent<mtx::events::msg::CallInvite>>(
- &e)) {
- callInvite->room_id = room_id_.toStdString();
- emit newCallEvent(e);
- } else if (std::holds_alternative<
- mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>(e) ||
- std::holds_alternative<
- mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>(e)) {
- emit newCallEvent(e);
- }
- }
-
- this->events.insert(id, e);
- ids.push_back(id);
-
- auto replyTo = mtx::accessors::in_reply_to_event(e);
- auto qReplyTo = QString::fromStdString(replyTo);
- if (!replyTo.empty() && !events.contains(qReplyTo)) {
- http::client()->get_event(
- this->room_id_.toStdString(),
- replyTo,
- [this, id, replyTo](
- const mtx::events::collections::TimelineEvents &timeline,
- mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->error(
- "Failed to retrieve event with id {}, which was "
- "requested to show the replyTo for event {}",
- replyTo,
- id.toStdString());
- return;
- }
- emit eventFetched(id, timeline);
- });
- }
- }
- return ids;
-}
-
void
TimelineModel::setCurrentIndex(int index)
{
@@ -863,7 +644,7 @@ TimelineModel::setCurrentIndex(int index)
currentId = indexToId(index);
emit currentIndexChanged(index);
- if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) &&
+ if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m") &&
ChatPage::instance()->isActiveWindow()) {
readEvent(currentId.toStdString());
}
@@ -881,27 +662,6 @@ TimelineModel::readEvent(const std::string &id)
});
}
-void
-TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
-{
- std::vector<QString> ids = internalAddEvents(msgs.chunk, false);
-
- if (!ids.empty()) {
- beginInsertRows(QModelIndex(),
- static_cast<int>(this->eventOrder.size()),
- static_cast<int>(this->eventOrder.size() + ids.size() - 1));
- this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
- endInsertRows();
- }
-
- prev_batch_token_ = QString::fromStdString(msgs.end);
-
- if (ids.empty() && !msgs.chunk.empty()) {
- // no visible events fetched, prevent loading from stopping
- fetchMore(QModelIndex());
- }
-}
-
QString
TimelineModel::displayName(QString id) const
{
@@ -938,7 +698,10 @@ TimelineModel::escapeEmoji(QString str) const
void
TimelineModel::viewRawMessage(QString id) const
{
- std::string ev = mtx::accessors::serialize_event(events.value(id)).dump(4);
+ auto e = events.get(id.toStdString(), "", false);
+ if (!e)
+ return;
+ std::string ev = mtx::accessors::serialize_event(*e).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
@@ -946,13 +709,11 @@ TimelineModel::viewRawMessage(QString id) const
void
TimelineModel::viewDecryptedRawMessage(QString id) const
{
- auto event = events.value(id);
- if (auto e =
- std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
- event = decryptEvent(*e).event;
- }
+ auto e = events.get(id.toStdString(), "");
+ if (!e)
+ return;
- std::string ev = mtx::accessors::serialize_event(event).dump(4);
+ std::string ev = mtx::accessors::serialize_event(*e).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
@@ -963,114 +724,6 @@ TimelineModel::openUserProfile(QString userid) const
MainWindow::instance()->openUserProfile(userid, room_id_);
}
-DecryptionResult
-TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const
-{
- static QCache<std::string, DecryptionResult> decryptedEvents{300};
-
- if (auto cachedEvent = decryptedEvents.object(e.event_id))
- return *cachedEvent;
-
- MegolmSessionIndex index;
- index.room_id = room_id_.toStdString();
- index.session_id = e.content.session_id;
- index.sender_key = e.content.sender_key;
-
- mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
- dummy.origin_server_ts = e.origin_server_ts;
- dummy.event_id = e.event_id;
- dummy.sender = e.sender;
- dummy.content.body =
- tr("-- Encrypted Event (No keys found for decryption) --",
- "Placeholder, when the message was not decrypted yet or can't be decrypted.")
- .toStdString();
-
- try {
- if (!cache::inboundMegolmSessionExists(index)) {
- nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
- index.room_id,
- index.session_id,
- e.sender);
- // TODO: request megolm session_id & session_key from the sender.
- decryptedEvents.insert(
- dummy.event_id, new DecryptionResult{dummy, false}, 1);
- return {dummy, false};
- }
- } catch (const lmdb::error &e) {
- nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
- dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
- "Placeholder, when the message can't be decrypted, because "
- "the DB access failed when trying to lookup the session.")
- .toStdString();
- decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
- return {dummy, false};
- }
-
- std::string msg_str;
- try {
- auto session = cache::getInboundMegolmSession(index);
- auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext);
- msg_str = std::string((char *)res.data.data(), res.data.size());
- } catch (const lmdb::error &e) {
- nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
- index.room_id,
- index.session_id,
- index.sender_key,
- e.what());
- dummy.content.body =
- tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
- "Placeholder, when the message can't be decrypted, because the DB access "
- "failed.")
- .toStdString();
- decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
- return {dummy, false};
- } catch (const mtx::crypto::olm_exception &e) {
- nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
- index.room_id,
- index.session_id,
- index.sender_key,
- e.what());
- dummy.content.body =
- tr("-- Decryption Error (%1) --",
- "Placeholder, when the message can't be decrypted. In this case, the Olm "
- "decrytion returned an error, which is passed ad %1.")
- .arg(e.what())
- .toStdString();
- decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
- return {dummy, false};
- }
-
- // Add missing fields for the event.
- json body = json::parse(msg_str);
- body["event_id"] = e.event_id;
- body["sender"] = e.sender;
- body["origin_server_ts"] = e.origin_server_ts;
- body["unsigned"] = e.unsigned_data;
-
- // relations are unencrypted in content...
- if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
- body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
-
- json event_array = json::array();
- event_array.push_back(body);
-
- std::vector<mtx::events::collections::TimelineEvents> temp_events;
- mtx::responses::utils::parse_timeline_events(event_array, temp_events);
-
- if (temp_events.size() == 1) {
- decryptedEvents.insert(e.event_id, new DecryptionResult{temp_events[0], true}, 1);
- return {temp_events[0], true};
- }
-
- dummy.content.body =
- tr("-- Encrypted Event (Unknown event type) --",
- "Placeholder, when the message was decrypted, but we couldn't parse it, because "
- "Nheko/mtxclient don't support that event type yet.")
- .toStdString();
- decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
- return {dummy, false};
-}
-
void
TimelineModel::replyAction(QString id)
{
@@ -1081,23 +734,18 @@ TimelineModel::replyAction(QString id)
RelatedInfo
TimelineModel::relatedInfo(QString id)
{
- if (!events.contains(id))
+ auto event = events.get(id.toStdString(), "");
+ if (!event)
return {};
- auto event = events.value(id);
- if (auto e =
- std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
- event = decryptEvent(*e).event;
- }
-
RelatedInfo related = {};
- related.quoted_user = QString::fromStdString(mtx::accessors::sender(event));
- related.related_event = mtx::accessors::event_id(event);
- related.type = mtx::accessors::msg_type(event);
+ related.quoted_user = QString::fromStdString(mtx::accessors::sender(*event));
+ related.related_event = mtx::accessors::event_id(*event);
+ related.type = mtx::accessors::msg_type(*event);
// get body, strip reply fallback, then transform the event to text, if it is a media event
// etc
- related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
+ related.quoted_body = QString::fromStdString(mtx::accessors::body(*event));
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
while (related.quoted_body.startsWith(">"))
related.quoted_body.remove(plainQuote);
@@ -1106,7 +754,7 @@ TimelineModel::relatedInfo(QString id)
related.quoted_body = utils::getQuoteBody(related);
// get quoted body and strip reply fallback
- related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
+ related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(*event);
related.quoted_formatted_body.remove(QRegularExpression(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
related.room = room_id_;
@@ -1144,18 +792,19 @@ TimelineModel::idToIndex(QString id) const
{
if (id.isEmpty())
return -1;
- for (int i = 0; i < (int)eventOrder.size(); i++)
- if (id == eventOrder[i])
- return i;
- return -1;
+
+ auto idx = events.idToIndex(id.toStdString());
+ if (idx)
+ return events.size() - *idx - 1;
+ else
+ return -1;
}
QString
TimelineModel::indexToId(int index) const
{
- if (index < 0 || index >= (int)eventOrder.size())
- return "";
- return eventOrder[index];
+ auto id = events.indexToId(events.size() - index - 1);
+ return id ? QString::fromStdString(*id) : "";
}
// Note: this will only be called for our messages
@@ -1189,28 +838,16 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
try {
// Check if we have already an outbound megolm session then we can use.
if (cache::outboundMegolmSessionExists(room_id)) {
- auto data =
+ mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
+ event.content =
olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
+ event.event_id = txn_id;
+ event.room_id = room_id;
+ event.sender = http::client()->user_id().to_string();
+ event.type = mtx::events::EventType::RoomEncrypted;
+ event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
- http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
- room_id,
- txn_id,
- data,
- [this, txn_id](const mtx::responses::EventId &res,
- mtx::http::RequestErr err) {
- if (err) {
- const int status_code =
- static_cast<int>(err->status_code);
- nhlog::net()->warn("[{}] failed to send message: {} {}",
- txn_id,
- err->matrix_error.error,
- status_code);
- emit messageFailed(QString::fromStdString(txn_id));
- }
- emit messageSent(
- QString::fromStdString(txn_id),
- QString::fromStdString(res.event_id.to_string()));
- });
+ emit this->addPendingMessageToStore(event);
return;
}
@@ -1239,40 +876,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
const auto members = cache::roomMembers(room_id);
nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
- auto keeper =
- std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() {
- try {
- auto data = olm::encrypt_group_message(
- room_id, http::client()->device_id(), doc);
+ auto keeper = std::make_shared<StateKeeper>([room_id, doc, txn_id, this]() {
+ try {
+ mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
+ event.content = olm::encrypt_group_message(
+ room_id, http::client()->device_id(), doc);
+ event.event_id = txn_id;
+ event.room_id = room_id;
+ event.sender = http::client()->user_id().to_string();
+ event.type = mtx::events::EventType::RoomEncrypted;
+ event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
- http::client()
- ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
- room_id,
- txn_id,
- data,
- [this, txn_id](const mtx::responses::EventId &res,
- mtx::http::RequestErr err) {
- if (err) {
- const int status_code =
- static_cast<int>(err->status_code);
- nhlog::net()->warn(
- "[{}] failed to send message: {} {}",
- txn_id,
- err->matrix_error.error,
- status_code);
- emit messageFailed(
- QString::fromStdString(txn_id));
- }
- emit messageSent(
- QString::fromStdString(txn_id),
- QString::fromStdString(res.event_id.to_string()));
- });
- } catch (const lmdb::error &e) {
- nhlog::db()->critical(
- "failed to save megolm outbound session: {}", e.what());
- emit messageFailed(QString::fromStdString(txn_id));
- }
- });
+ emit this->addPendingMessageToStore(event);
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("failed to save megolm outbound session: {}",
+ e.what());
+ emit ChatPage::instance()->showNotification(
+ tr("Failed to encrypt event, sending aborted!"));
+ }
+ });
mtx::requests::QueryKeys req;
for (const auto &member : members)
@@ -1286,8 +908,8 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
nhlog::net()->warn("failed to query device keys: {} {}",
err->matrix_error.error,
static_cast<int>(err->status_code));
- // TODO: Mark the event as failed. Communicate with the UI.
- emit messageFailed(QString::fromStdString(txn_id));
+ emit ChatPage::instance()->showNotification(
+ tr("Failed to encrypt event, sending aborted!"));
return;
}
@@ -1387,11 +1009,13 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
} catch (const lmdb::error &e) {
nhlog::db()->critical(
"failed to open outbound megolm session ({}): {}", room_id, e.what());
- emit messageFailed(QString::fromStdString(txn_id));
+ emit ChatPage::instance()->showNotification(
+ tr("Failed to encrypt event, sending aborted!"));
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical(
"failed to open outbound megolm session ({}): {}", room_id, e.what());
- emit messageFailed(QString::fromStdString(txn_id));
+ emit ChatPage::instance()->showNotification(
+ tr("Failed to encrypt event, sending aborted!"));
}
}
@@ -1483,13 +1107,12 @@ TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
struct SendMessageVisitor
{
- SendMessageVisitor(const QString &txn_id, TimelineModel *model)
- : txn_id_qstr_(txn_id)
- , model_(model)
+ explicit SendMessageVisitor(TimelineModel *model)
+ : model_(model)
{}
template<typename T, mtx::events::EventType Event>
- void sendRoomEvent(const mtx::events::RoomEvent<T> &msg)
+ void sendRoomEvent(mtx::events::RoomEvent<T> msg)
{
if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
auto encInfo = mtx::accessors::file(msg);
@@ -1497,36 +1120,13 @@ struct SendMessageVisitor
emit model_->newEncryptedImage(encInfo.value());
model_->sendEncryptedMessageEvent(
- txn_id_qstr_.toStdString(), nlohmann::json(msg.content), Event);
+ msg.event_id, nlohmann::json(msg.content), Event);
} else {
- sendUnencryptedRoomEvent<T, Event>(msg);
+ msg.type = Event;
+ emit model_->addPendingMessageToStore(msg);
}
}
- template<typename T, mtx::events::EventType Event>
- void sendUnencryptedRoomEvent(const mtx::events::RoomEvent<T> &msg)
- {
- QString txn_id_qstr = txn_id_qstr_;
- TimelineModel *model = model_;
- http::client()->send_room_message<T, Event>(
- model->room_id_.toStdString(),
- txn_id_qstr.toStdString(),
- msg.content,
- [txn_id_qstr, model](const mtx::responses::EventId &res,
- mtx::http::RequestErr err) {
- if (err) {
- const int status_code = static_cast<int>(err->status_code);
- nhlog::net()->warn("[{}] failed to send message: {} {}",
- txn_id_qstr.toStdString(),
- err->matrix_error.error,
- status_code);
- emit model->messageFailed(txn_id_qstr);
- }
- emit model->messageSent(txn_id_qstr,
- QString::fromStdString(res.event_id.to_string()));
- });
- }
-
// Do-nothing operator for all unhandled events
template<typename T>
void operator()(const mtx::events::Event<T> &)
@@ -1535,7 +1135,7 @@ struct SendMessageVisitor
// Operator for m.room.message events that contain a msgtype in their content
template<typename T,
std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
- void operator()(const mtx::events::RoomEvent<T> &msg)
+ void operator()(mtx::events::RoomEvent<T> msg)
{
sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg);
}
@@ -1545,10 +1145,10 @@ struct SendMessageVisitor
// reactions need to have the relation outside of ciphertext, or synapse / the homeserver
// cannot handle it correctly. See the MSC for more details:
// https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
- void operator()(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &msg)
+ void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
{
- sendUnencryptedRoomEvent<mtx::events::msg::Reaction,
- mtx::events::EventType::Reaction>(msg);
+ msg.type = mtx::events::EventType::Reaction;
+ emit model_->addPendingMessageToStore(msg);
}
void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event)
@@ -1575,65 +1175,38 @@ struct SendMessageVisitor
event);
}
- QString txn_id_qstr_;
TimelineModel *model_;
};
void
-TimelineModel::processOnePendingMessage()
-{
- if (pending.isEmpty())
- return;
-
- QString txn_id_qstr = pending.first();
-
- auto event = events.value(txn_id_qstr);
- std::visit(SendMessageVisitor{txn_id_qstr, this}, event);
-}
-
-void
TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
std::visit(
[](auto &msg) {
- msg.event_id = http::client()->generate_txn_id();
+ msg.type = mtx::events::EventType::RoomMessage;
+ msg.event_id = "m" + http::client()->generate_txn_id();
msg.sender = http::client()->user_id().to_string();
msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
},
event);
- internalAddEvents({event}, false);
-
- QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event));
- pending.push_back(txn_id_qstr);
- if (!std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&event) &&
- !std::get_if<mtx::events::RoomEvent<mtx::events::msg::CallCandidates>>(&event)) {
- beginInsertRows(QModelIndex(), 0, 0);
- this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr);
- endInsertRows();
- }
- updateLastMessage();
-
- emit nextPendingMessage();
+ std::visit(SendMessageVisitor{this}, event);
}
bool
TimelineModel::saveMedia(QString eventId) const
{
- mtx::events::collections::TimelineEvents event = events.value(eventId);
-
- if (auto e =
- std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
- event = decryptEvent(*e).event;
- }
+ mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
+ if (!event)
+ return false;
- QString mxcUrl = QString::fromStdString(mtx::accessors::url(event));
- QString originalFilename = QString::fromStdString(mtx::accessors::filename(event));
- QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event));
+ QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event));
+ QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+ QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event));
- auto encryptionInfo = mtx::accessors::file(event);
+ auto encryptionInfo = mtx::accessors::file(*event);
- qml_mtx_events::EventType eventType = toRoomEventType(event);
+ qml_mtx_events::EventType eventType = toRoomEventType(*event);
QString dialogTitle;
if (eventType == qml_mtx_events::EventType::ImageMessage) {
@@ -1698,18 +1271,15 @@ TimelineModel::saveMedia(QString eventId) const
void
TimelineModel::cacheMedia(QString eventId)
{
- mtx::events::collections::TimelineEvents event = events.value(eventId);
-
- if (auto e =
- std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
- event = decryptEvent(*e).event;
- }
+ mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
+ if (!event)
+ return;
- QString mxcUrl = QString::fromStdString(mtx::accessors::url(event));
- QString originalFilename = QString::fromStdString(mtx::accessors::filename(event));
- QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event));
+ QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event));
+ QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
+ QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event));
- auto encryptionInfo = mtx::accessors::file(event);
+ auto encryptionInfo = mtx::accessors::file(*event);
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith("mxc://")) {
@@ -1830,11 +1400,11 @@ TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg)
QString
TimelineModel::formatJoinRuleEvent(QString id)
{
- if (!events.contains(id))
+ mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+ if (!e)
return "";
- auto event =
- std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(&events[id]);
+ auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e);
if (!event)
return "";
@@ -1855,11 +1425,11 @@ TimelineModel::formatJoinRuleEvent(QString id)
QString
TimelineModel::formatGuestAccessEvent(QString id)
{
- if (!events.contains(id))
+ mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+ if (!e)
return "";
- auto event =
- std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(&events[id]);
+ auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
if (!event)
return "";
@@ -1879,11 +1449,11 @@ TimelineModel::formatGuestAccessEvent(QString id)
QString
TimelineModel::formatHistoryVisibilityEvent(QString id)
{
- if (!events.contains(id))
+ mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+ if (!e)
return "";
- auto event =
- std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(&events[id]);
+ auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
if (!event)
return "";
@@ -1913,11 +1483,11 @@ TimelineModel::formatHistoryVisibilityEvent(QString id)
QString
TimelineModel::formatPowerLevelEvent(QString id)
{
- if (!events.contains(id))
+ mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+ if (!e)
return "";
- auto event =
- std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(&events[id]);
+ auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
if (!event)
return "";
@@ -1931,37 +1501,22 @@ TimelineModel::formatPowerLevelEvent(QString id)
QString
TimelineModel::formatMemberEvent(QString id)
{
- if (!events.contains(id))
+ mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
+ if (!e)
return "";
- auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&events[id]);
+ auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
if (!event)
return "";
mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr;
- QString prevEventId = QString::fromStdString(event->unsigned_data.replaces_state);
- if (!prevEventId.isEmpty()) {
- if (!events.contains(prevEventId)) {
- http::client()->get_event(
- this->room_id_.toStdString(),
- event->unsigned_data.replaces_state,
- [this, id, prevEventId](
- const mtx::events::collections::TimelineEvents &timeline,
- mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->error(
- "Failed to retrieve event with id {}, which was "
- "requested to show the membership for event {}",
- prevEventId.toStdString(),
- id.toStdString());
- return;
- }
- emit eventFetched(id, timeline);
- });
- } else {
+ if (!event->unsigned_data.replaces_state.empty()) {
+ auto tempPrevEvent =
+ events.get(event->unsigned_data.replaces_state, event->event_id);
+ if (tempPrevEvent) {
prevEvent =
std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(
- &events[prevEventId]);
+ tempPrevEvent);
}
}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 95584d36..156606e6 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -9,7 +9,7 @@
#include <mtxclient/http/errors.hpp>
#include "CacheCryptoStructs.h"
-#include "ReactionsModel.h"
+#include "EventStore.h"
namespace mtx::http {
using RequestErr = const std::optional<mtx::http::ClientError> &;
@@ -42,6 +42,8 @@ enum EventType
CallAnswer,
/// m.call.hangup
CallHangUp,
+ /// m.call.candidates
+ CallCandidates,
/// m.room.canonical_alias
CanonicalAlias,
/// m.room.create
@@ -177,7 +179,7 @@ public:
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
- QVariant data(const QString &id, int role) const;
+ QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
bool canFetchMore(const QModelIndex &) const override;
void fetchMore(const QModelIndex &) override;
@@ -204,6 +206,15 @@ public:
Q_INVOKABLE void cacheMedia(QString eventId);
Q_INVOKABLE bool saveMedia(QString eventId) const;
+ std::vector<::Reaction> reactions(const std::string &event_id)
+ {
+ auto list = events.reactions(event_id);
+ std::vector<::Reaction> vec;
+ for (const auto &r : list)
+ vec.push_back(r.value<Reaction>());
+ return vec;
+ }
+
void updateLastMessage();
void addEvents(const mtx::responses::Timeline &events);
template<class T>
@@ -214,7 +225,7 @@ public slots:
void setCurrentIndex(int index);
int currentIndex() const { return idToIndex(currentId); }
void markEventsAsRead(const std::vector<QString> &event_ids);
- QVariantMap getDump(QString eventId) const;
+ QVariantMap getDump(QString eventId, QString relatedTo) const;
void updateTypingUsers(const std::vector<QString> &users)
{
if (this->typingUsers_ != users) {
@@ -240,36 +251,26 @@ public slots:
}
}
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
+ void clearTimeline() { events.clearTimeline(); }
private slots:
- // Add old events at the top of the timeline.
- void addBackwardsEvents(const mtx::responses::Messages &msgs);
- void processOnePendingMessage();
void addPendingMessage(mtx::events::collections::TimelineEvents event);
signals:
- void oldMessagesRetrieved(const mtx::responses::Messages &res);
- void messageFailed(QString txn_id);
- void messageSent(QString txn_id, QString event_id);
void currentIndexChanged(int index);
void redactionFailed(QString id);
void eventRedacted(QString id);
- void nextPendingMessage();
- void newMessageToSend(mtx::events::collections::TimelineEvents event);
void mediaCached(QString mxcUrl, QString cacheUrl);
void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
- void eventFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event);
void typingUsersChanged(std::vector<QString> users);
void replyChanged(QString reply);
void paginationInProgressChanged(const bool);
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
+ void newMessageToSend(mtx::events::collections::TimelineEvents event);
+ void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
+
private:
- DecryptionResult decryptEvent(
- const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const;
- std::vector<QString> internalAddEvents(
- const std::vector<mtx::events::collections::TimelineEvents> &timeline,
- bool emitCallEvents);
void sendEncryptedMessageEvent(const std::string &txn_id,
nlohmann::json content,
mtx::events::EventType);
@@ -283,16 +284,12 @@ private:
void setPaginationInProgress(const bool paginationInProgress);
- QHash<QString, mtx::events::collections::TimelineEvents> events;
QSet<QString> read;
- QList<QString> pending;
- std::vector<QString> eventOrder;
- std::map<QString, ReactionsModel> reactions;
+
+ mutable EventStore events;
QString room_id_;
- QString prev_batch_token_;
- bool isInitialSync = true;
bool decryptDescription = true;
bool m_paginationInProgress = false;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 5d0b54c3..466c3cee 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -340,36 +340,39 @@ TimelineViewManager::queueEmoteMessage(const QString &msg)
}
void
-TimelineViewManager::reactToMessage(const QString &roomId,
- const QString &reactedEvent,
- const QString &reactionKey,
- const QString &selfReactedEvent)
+TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
{
+ if (!timeline_)
+ return;
+
+ auto reactions = timeline_->reactions(reactedEvent.toStdString());
+
+ QString selfReactedEvent;
+ for (const auto &reaction : reactions) {
+ if (reactionKey == reaction.key_) {
+ selfReactedEvent = reaction.selfReactedEvent_;
+ break;
+ }
+ }
+
+ if (selfReactedEvent.startsWith("m"))
+ return;
+
// If selfReactedEvent is empty, that means we haven't previously reacted
if (selfReactedEvent.isEmpty()) {
- queueReactionMessage(roomId, reactedEvent, reactionKey);
+ mtx::events::msg::Reaction reaction;
+ reaction.relates_to.rel_type = mtx::common::RelationType::Annotation;
+ reaction.relates_to.event_id = reactedEvent.toStdString();
+ reaction.relates_to.key = reactionKey.toStdString();
+
+ timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
// Otherwise, we have previously reacted and the reaction should be redacted
} else {
- auto model = models.value(roomId);
- model->redactEvent(selfReactedEvent);
+ timeline_->redactEvent(selfReactedEvent);
}
}
void
-TimelineViewManager::queueReactionMessage(const QString &roomId,
- const QString &reactedEvent,
- const QString &reactionKey)
-{
- mtx::events::msg::Reaction reaction;
- reaction.relates_to.rel_type = mtx::common::RelationType::Annotation;
- reaction.relates_to.event_id = reactedEvent.toStdString();
- reaction.relates_to.key = reactionKey.toStdString();
-
- auto model = models.value(roomId);
- model->sendMessageEvent(reaction, mtx::events::EventType::RoomMessage);
-}
-
-void
TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
@@ -384,10 +387,13 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
image.info.size = dsize;
image.info.blurhash = blurhash.toStdString();
image.body = filename.toStdString();
- image.url = url.toStdString();
image.info.h = dimensions.height();
image.info.w = dimensions.width();
- image.file = file;
+
+ if (file)
+ image.file = file;
+ else
+ image.url = url.toStdString();
auto model = models.value(roomid);
if (!model->reply().isEmpty()) {
@@ -411,8 +417,11 @@ TimelineViewManager::queueFileMessage(
file.info.mimetype = mime.toStdString();
file.info.size = dsize;
file.body = filename.toStdString();
- file.url = url.toStdString();
- file.file = encryptedFile;
+
+ if (encryptedFile)
+ file.file = encryptedFile;
+ else
+ file.url = url.toStdString();
auto model = models.value(roomid);
if (!model->reply().isEmpty()) {
@@ -436,7 +445,11 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
audio.info.size = dsize;
audio.body = filename.toStdString();
audio.url = url.toStdString();
- audio.file = file;
+
+ if (file)
+ audio.file = file;
+ else
+ audio.url = url.toStdString();
auto model = models.value(roomid);
if (!model->reply().isEmpty()) {
@@ -459,8 +472,11 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
video.info.mimetype = mime.toStdString();
video.info.size = dsize;
video.body = filename.toStdString();
- video.url = url.toStdString();
- video.file = file;
+
+ if (file)
+ video.file = file;
+ else
+ video.url = url.toStdString();
auto model = models.value(roomid);
if (!model->reply().isEmpty()) {
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 902dc047..ea6d1743 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -66,13 +66,7 @@ public slots:
void setHistoryView(const QString &room_id);
void updateColorPalette();
- void queueReactionMessage(const QString &roomId,
- const QString &reactedEvent,
- const QString &reaction);
- void reactToMessage(const QString &roomId,
- const QString &reactedEvent,
- const QString &reactionKey,
- const QString &selfReactedEvent);
+ void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey);
void queueTextMessage(const QString &msg);
void queueEmoteMessage(const QString &msg);
void queueImageMessage(const QString &roomid,
@@ -108,6 +102,12 @@ public slots:
void updateEncryptedDescriptions();
+ void clearCurrentRoomTimeline()
+ {
+ if (timeline_)
+ timeline_->clearTimeline();
+ }
+
private:
#ifdef USE_QUICK_VIEW
QQuickView *view;
|