// SPDX-FileCopyrightText: Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later #include "Cache.h" #include "Cache_p.h" #include #include #include #include #include #include #include #include #include #include #include #if __has_include() #include #else #include #endif #include #include #include #include "ChatPage.h" #include "EventAccessors.h" #include "Logging.h" #include "MatrixClient.h" #include "UserSettingsPage.h" #include "Utils.h" #include "encryption/Olm.h" //! Should be changed when a breaking change occurs in the cache format. //! This will reset client's data. static constexpr std::string_view CURRENT_CACHE_FORMAT_VERSION{"2023.10.22"}; static constexpr std::string_view MAX_DBS_SETTINGS_KEY{"database/maxdbs"}; static constexpr std::string_view MAX_DB_SIZE_SETTINGS_KEY{"database/maxsize"}; //! Keys used for the DB static const std::string_view NEXT_BATCH_KEY("next_batch"); static const std::string_view OLM_ACCOUNT_KEY("olm_account"); static const std::string_view CACHE_FORMAT_VERSION_KEY("cache_format_version"); static const std::string_view CURRENT_ONLINE_BACKUP_VERSION("current_online_backup_version"); static constexpr auto MAX_DBS_DEFAULT = 32384U; #if Q_PROCESSOR_WORDSIZE >= 5 // 40-bit or more, up to 2^(8*WORDSIZE) words addressable. static constexpr auto DB_SIZE_DEFAULT = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB static constexpr size_t MAX_RESTORED_MESSAGES = 30'000; #elif Q_PROCESSOR_WORDSIZE == 4 // 32-bit address space limits mmaps static constexpr auto DB_SIZE_DEFAULT = 1ULL * 1024ULL * 1024ULL * 1024ULL; // 1 GB static constexpr size_t MAX_RESTORED_MESSAGES = 5'000; #else #error Not enough virtual address space for the database on target CPU #endif //! Cache databases and their format. //! //! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc). //! Format: room_id -> RoomInfo static constexpr auto ROOMS_DB("rooms"); static constexpr auto INVITES_DB("invites"); //! maps each room to its parent space (id->id) static constexpr auto SPACES_PARENTS_DB("space_parents"); //! maps each space to its current children (id->id) static constexpr auto SPACES_CHILDREN_DB("space_children"); //! Information that must be kept between sync requests. static constexpr auto SYNC_STATE_DB("sync_state"); //! Read receipts per room/event. static constexpr auto READ_RECEIPTS_DB("read_receipts"); static constexpr auto NOTIFICATIONS_DB("sent_notifications"); static constexpr auto PRESENCE_DB("presence"); //! Encryption related databases. //! user_id -> list of devices static constexpr auto DEVICES_DB("devices"); //! device_id -> device keys static constexpr auto DEVICE_KEYS_DB("device_keys"); //! room_ids that have encryption enabled. static constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); //! Expiration progress for each room static constexpr auto EVENT_EXPIRATION_BG_JOB_DB("event_expiration_bg_job"); //! room_id -> pickled OlmInboundGroupSession static constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); //! MegolmSessionIndex -> pickled OlmOutboundGroupSession static constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); //! MegolmSessionIndex -> session data about which devices have access to this static constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db"); //! Curve25519 key to session_id and json encoded olm session, separated by null. Dupsorted. static constexpr auto OLM_SESSIONS_DB("olm_sessions.v3"); //! flag to be set, when the db should be compacted on startup bool needsCompact = false; using CachedReceipts = std::multimap>; using Receipts = std::map>; static std::string combineOlmSessionKeyFromCurveAndSessionId(std::string_view curve25519, std::string_view session_id) { std::string combined(curve25519.size() + 1 + session_id.size(), '\0'); combined.replace(0, curve25519.size(), curve25519); combined.replace(curve25519.size() + 1, session_id.size(), session_id); return combined; } static std::pair splitCurve25519AndOlmSessionId(std::string_view input) { auto separator = input.find('\0'); return std::pair(input.substr(0, separator), input.substr(separator + 1)); } namespace { std::unique_ptr instance_ = nullptr; } struct RO_txn { ~RO_txn() { txn.reset(); } operator MDB_txn *() const noexcept { return txn.handle(); } operator lmdb::txn &() noexcept { return txn; } lmdb::txn &txn; }; RO_txn ro_txn(lmdb::env &env) { thread_local lmdb::txn txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); thread_local int reuse_counter = 0; if (reuse_counter >= 100 || txn.env() != env.handle()) { txn.abort(); txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); reuse_counter = 0; } else if (reuse_counter > 0) { try { txn.renew(); } catch (...) { txn.abort(); txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); reuse_counter = 0; } } reuse_counter++; return RO_txn{txn}; } static void compactDatabase(lmdb::env &from, lmdb::env &to) { auto fromTxn = lmdb::txn::begin(from, nullptr, MDB_RDONLY); auto toTxn = lmdb::txn::begin(to); auto rootDb = lmdb::dbi::open(fromTxn); auto dbNames = lmdb::cursor::open(fromTxn, rootDb); std::string_view dbName; while (dbNames.get(dbName, MDB_cursor_op::MDB_NEXT_NODUP)) { nhlog::db()->info("Compacting db: {}", dbName); auto flags = MDB_CREATE; if (dbName.ends_with("/event_order") || dbName.ends_with("/order2msg") || dbName.ends_with("/pending")) flags |= MDB_INTEGERKEY; if (dbName.ends_with("/related") || dbName.ends_with("/states_key") || dbName == SPACES_CHILDREN_DB || dbName == SPACES_PARENTS_DB) flags |= MDB_DUPSORT; auto dbNameStr = std::string(dbName); auto fromDb = lmdb::dbi::open(fromTxn, dbNameStr.c_str(), flags); auto toDb = lmdb::dbi::open(toTxn, dbNameStr.c_str(), flags); if (dbName.ends_with("/states_key")) { lmdb::dbi_set_dupsort(fromTxn, fromDb, Cache::compare_state_key); lmdb::dbi_set_dupsort(toTxn, toDb, Cache::compare_state_key); } auto fromCursor = lmdb::cursor::open(fromTxn, fromDb); auto toCursor = lmdb::cursor::open(toTxn, toDb); std::string_view key, val; while (fromCursor.get(key, val, MDB_cursor_op::MDB_NEXT)) { toCursor.put(key, val, MDB_APPENDDUP); } } toTxn.commit(); } static bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) { using namespace mtx::events; using namespace mtx::events::state; return std::holds_alternative>(e) || std::holds_alternative>(e) || std::holds_alternative>(e) || std::holds_alternative>(e) || std::holds_alternative>(e); } bool Cache::isHiddenEvent(lmdb::txn &txn, mtx::events::collections::TimelineEvents e, const std::string &room_id) { using namespace mtx::events; // Always hide edits if (mtx::accessors::relations(e).replaces()) return true; if (auto encryptedEvent = std::get_if>(&e)) { MegolmSessionIndex index; index.room_id = room_id; index.session_id = encryptedEvent->content.session_id; auto result = olm::decryptEvent(index, *encryptedEvent, true); if (!result.error) e = result.event.value(); } mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents; hiddenEvents.hidden_event_types = std::vector{ EventType::Reaction, EventType::CallCandidates, EventType::CallNegotiate, EventType::Unsupported, }; // check if selected answer is from to local user /* * localUser accepts/rejects the call and it is selected by caller - No message * Another User accepts/rejects the call and it is selected by caller - "Call answered/rejected * elsewhere" */ bool callLocalUser_ = true; if (callLocalUser_) hiddenEvents.hidden_event_types->push_back(EventType::CallSelectAnswer); if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, "")) { auto h = std::get< mtx::events::AccountDataEvent>( *temp); if (h.content.hidden_event_types) hiddenEvents = std::move(h.content); } if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id)) { auto h = std::get< mtx::events::AccountDataEvent>( *temp); if (h.content.hidden_event_types) hiddenEvents = std::move(h.content); } return std::find(hiddenEvents.hidden_event_types->begin(), hiddenEvents.hidden_event_types->end(), std::visit([](const auto &ev) { return ev.type; }, e)) != hiddenEvents.hidden_event_types->end(); } Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} , env_{nullptr} , localUserId_{userId} { connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); connect( this, &Cache::verificationStatusChanged, this, [this](const std::string &u) { if (u == localUserId_.toStdString()) { auto status = verificationStatus(u); emit selfVerificationStatusChanged(); } }, Qt::QueuedConnection); setup(); } static QString cacheDirectoryName(const QString &userid, const QString &profile) { QCryptographicHash hash(QCryptographicHash::Algorithm::Sha256); hash.addData(userid.toUtf8()); hash.addData(profile.toUtf8()); return QStringLiteral("%1/db-%2") .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), hash.result().toHex()); } void Cache::setup() { auto settings = UserSettings::instance(); nhlog::db()->debug("setting up cache"); // Previous location of the cache directory auto oldCache2 = QStringLiteral("%1/%2%3").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), QString::fromUtf8(localUserId_.toUtf8().toHex()), QString::fromUtf8(settings->profile().toUtf8().toHex())); auto oldCache = QStringLiteral("%1/%2%3").arg( QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), QString::fromUtf8(localUserId_.toUtf8().toHex()), QString::fromUtf8(settings->profile().toUtf8().toHex())); cacheDirectory_ = cacheDirectoryName(localUserId_, settings->profile()); nhlog::db()->debug("Database at: {}", cacheDirectory_.toStdString()); bool isInitial = !QFile::exists(cacheDirectory_); // NOTE: If both cache directories exist it's better to do nothing: it // could mean a previous migration failed or was interrupted. if (isInitial) { if (QFile::exists(oldCache)) { nhlog::db()->info("found old state directory, migrating"); if (!QDir().rename(oldCache, cacheDirectory_)) { throw std::runtime_error(("Unable to migrate the old state directory (" + oldCache + ") to the new location (" + cacheDirectory_ + ")") .toStdString() .c_str()); } nhlog::db()->info("completed state migration"); } else if (QFile::exists(oldCache2)) { nhlog::db()->info("found very old state directory, migrating"); if (!QDir().rename(oldCache2, cacheDirectory_)) { throw std::runtime_error(("Unable to migrate the very old state directory (" + oldCache2 + ") to the new location (" + cacheDirectory_ + ")") .toStdString() .c_str()); } nhlog::db()->info("completed state migration"); } } auto openEnv = [](const QString &name) { auto settings = UserSettings::instance(); std::size_t dbSize = std::max( settings->qsettings()->value(MAX_DB_SIZE_SETTINGS_KEY, DB_SIZE_DEFAULT).toULongLong(), DB_SIZE_DEFAULT); unsigned dbCount = std::max(settings->qsettings()->value(MAX_DBS_SETTINGS_KEY, MAX_DBS_DEFAULT).toUInt(), MAX_DBS_DEFAULT); // ignore unreasonably high values of more than a quarter of the addressable memory if (dbSize > (1ull << (Q_PROCESSOR_WORDSIZE * 8 - 2))) { dbSize = DB_SIZE_DEFAULT; } // Limit databases to about a million. This would cause more than 7-120MB to get written on // every commit, which I doubt would work well. File an issue, if you tested this and it // works fine. if (dbCount > (1u << 20)) { dbCount = 1u << 20; } auto e = lmdb::env::create(); e.set_mapsize(dbSize); e.set_max_dbs(dbCount); e.open(name.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC); return e; }; if (isInitial) { nhlog::db()->info("initializing LMDB"); if (!QDir().mkpath(cacheDirectory_)) { throw std::runtime_error( ("Unable to create state directory:" + cacheDirectory_).toStdString().c_str()); } } try { // 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. // // 2022-10-28: Disable the nosync flags again in the hope to crack down on some database // corruption. // 2023-02-23: Reenable the nosync flags. There was no measureable benefit to resiliency, // but sync causes frequent lag sometimes even for the whole system. Possibly the data // corruption is an lmdb or filesystem bug. See // https://github.com/Nheko-Reborn/nheko/issues/1355 // https://github.com/Nheko-Reborn/nheko/issues/1303 env_ = openEnv(cacheDirectory_); if (needsCompact) { auto compactDir = cacheDirectory_ + "-compacting"; auto toDeleteDir = cacheDirectory_ + "-olddb"; if (QFile::exists(cacheDirectory_)) QDir(compactDir).removeRecursively(); if (QFile::exists(toDeleteDir)) QDir(toDeleteDir).removeRecursively(); if (!QDir().mkpath(compactDir)) { nhlog::db()->warn( "Failed to create directory '{}' for database compaction, skipping compaction!", compactDir.toStdString()); } else { // lmdb::env_copy(env_, compactDir.toStdString().c_str(), MDB_CP_COMPACT); // create a temporary db auto temp = openEnv(compactDir); // copy data compactDatabase(env_, temp); // close envs temp.close(); env_.close(); // swap the databases and delete old one QDir().rename(cacheDirectory_, toDeleteDir); QDir().rename(compactDir, cacheDirectory_); QDir(toDeleteDir).removeRecursively(); // reopen env env_ = openEnv(cacheDirectory_); } } } catch (const lmdb::error &e) { if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { throw std::runtime_error("LMDB initialization failed" + std::string(e.what())); } nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); QDir stateDir(cacheDirectory_); auto eList = stateDir.entryList(QDir::NoDotAndDotDot); for (const auto &file : std::as_const(eList)) { if (!stateDir.remove(file)) throw std::runtime_error(("Unable to delete file " + file).toStdString().c_str()); } env_ = openEnv(cacheDirectory_); } auto txn = lmdb::txn::begin(env_); syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT); spacesParentsDb_ = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT); invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); presenceDb_ = lmdb::dbi::open(txn, PRESENCE_DB, MDB_CREATE); // Device management devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE); // Session management inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); olmSessionDb_ = lmdb::dbi::open(txn, OLM_SESSIONS_DB, MDB_CREATE); // What rooms are encrypted encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); eventExpiryBgJob_ = lmdb::dbi::open(txn, EVENT_EXPIRATION_BG_JOB_DB, MDB_CREATE); [[maybe_unused]] auto verificationDb = getVerificationDb(txn); [[maybe_unused]] auto userKeysDb = getUserKeysDb(txn); txn.commit(); loadSecretsFromStore( { {"pickle_secret", true}, }, [this](const std::string &, bool, const std::string &value) { this->pickle_secret_ = value; }, true); } static void fatalSecretError() { QMessageBox::critical( nullptr, QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"), QCoreApplication::translate( "SecretStorage", "Nheko could not connect to the secure storage to save encryption secrets to. This can " "have multiple reasons. Check if your D-Bus service is running and you have configured a " "service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If " "you are having trouble, feel free to open an issue here: " "https://github.com/Nheko-Reborn/nheko/issues"), QMessageBox::StandardButton::Close); QCoreApplication::exit(1); exit(1); } static QString secretName(std::string_view name, bool internal) { auto settings = UserSettings::instance(); return (internal ? "nheko." : "matrix.") + QString( QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256) .toBase64()) + "." + QString::fromUtf8(name); } void Cache::loadSecretsFromStore( std::vector> toLoad, std::function callback, bool databaseReadyOnFinished) { auto settings = UserSettings::instance()->qsettings(); if (toLoad.empty()) { this->databaseReady_ = true; // HACK(Nico): Some migrations would loop infinitely otherwise. // So we set the database to be ready, but not emit the signal, because that would start the // migrations again. :D if (databaseReadyOnFinished) { emit databaseReady(); nhlog::db()->debug("Database ready"); } return; } if (settings->value(QStringLiteral("run_without_secure_secrets_service"), false).toBool()) { for (auto &[name_, internal] : toLoad) { auto name = secretName(name_, internal); auto value = settings->value("secrets/" + name).toString(); if (value.isEmpty()) { nhlog::db()->info("Restored empty secret '{}'.", name.toStdString()); } else { callback(name_, internal, value.toStdString()); } } // if we emit the DatabaseReady signal directly it won't be received QTimer::singleShot(0, this, [this, callback, databaseReadyOnFinished] { loadSecretsFromStore({}, callback, databaseReadyOnFinished); }); return; } auto [name_, internal] = toLoad.front(); auto job = new QKeychain::ReadPasswordJob(QCoreApplication::applicationName()); job->setAutoDelete(true); job->setInsecureFallback(true); job->setSettings(settings); auto name = secretName(name_, internal); job->setKey(name); connect(job, &QKeychain::ReadPasswordJob::finished, this, [this, name, toLoad, job, name__ = name_, internal_ = internal, callback, databaseReadyOnFinished](QKeychain::Job *) mutable { nhlog::db()->debug("Finished reading '{}'", toLoad.begin()->first); const QString secret = job->textData(); if (job->error() && job->error() != QKeychain::Error::EntryNotFound) { nhlog::db()->error("Restoring secret '{}' failed ({}): {}", name.toStdString(), static_cast(job->error()), job->errorString().toStdString()); fatalSecretError(); } if (secret.isEmpty()) { nhlog::db()->debug("Restored empty secret '{}'.", name.toStdString()); } else { callback(name__, internal_, secret.toStdString()); } // load next secret toLoad.erase(toLoad.begin()); // You can't start a job from the finish signal of a job. QTimer::singleShot(0, this, [this, toLoad, callback, databaseReadyOnFinished] { loadSecretsFromStore(toLoad, callback, databaseReadyOnFinished); }); }); nhlog::db()->debug("Reading '{}'", name_); job->start(); } std::optional Cache::secret(std::string_view name_, bool internal) { auto name = secretName(name_, internal); auto txn = ro_txn(env_); std::string_view value; auto db_name = "secret." + name.toStdString(); if (!syncStateDb_.get(txn, db_name, value)) return std::nullopt; mtx::secret_storage::AesHmacSha2EncryptedData data = nlohmann::json::parse(value); auto decrypted = mtx::crypto::decrypt(data, mtx::crypto::to_binary_buf(pickle_secret_), name_); if (decrypted.empty()) return std::nullopt; else return decrypted; } void Cache::storeSecret(std::string_view name_, const std::string &secret, bool internal) { auto name = secretName(name_, internal); auto txn = lmdb::txn::begin(env_); auto encrypted = mtx::crypto::encrypt(secret, mtx::crypto::to_binary_buf(pickle_secret_), name_); auto db_name = "secret." + name.toStdString(); syncStateDb_.put(txn, db_name, nlohmann::json(encrypted).dump()); txn.commit(); emit secretChanged(std::string(name_)); } void Cache::deleteSecret(std::string_view name_, bool internal) { auto name = secretName(name_, internal); auto txn = lmdb::txn::begin(env_); std::string_view value; auto db_name = "secret." + name.toStdString(); syncStateDb_.del(txn, db_name, value); txn.commit(); } void Cache::storeSecretInStore(const std::string name_, const std::string secret) { auto name = secretName(name_, true); auto settings = UserSettings::instance()->qsettings(); if (settings->value(QStringLiteral("run_without_secure_secrets_service"), false).toBool()) { settings->setValue("secrets/" + name, QString::fromStdString(secret)); // if we emit the signal directly it won't be received QTimer::singleShot(0, this, [this, name_] { emit secretChanged(name_); }); nhlog::db()->info("Storing secret '{}' successful", name_); return; } auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName()); job->setAutoDelete(true); job->setInsecureFallback(true); job->setSettings(settings); job->setKey(name); job->setTextData(QString::fromStdString(secret)); QObject::connect( job, &QKeychain::WritePasswordJob::finished, this, [name_, this](QKeychain::Job *job) { if (job->error()) { nhlog::db()->warn( "Storing secret '{}' failed: {}", name_, job->errorString().toStdString()); fatalSecretError(); } else { // if we emit the signal directly, qtkeychain breaks and won't execute new // jobs. You can't start a job from the finish signal of a job. QTimer::singleShot(0, this, [this, name_] { emit secretChanged(name_); }); nhlog::db()->info("Storing secret '{}' successful", name_); } }, Qt::ConnectionType::DirectConnection); job->start(); } void Cache::deleteSecretFromStore(const std::string name, bool internal) { auto name_ = secretName(name, internal); auto settings = UserSettings::instance()->qsettings(); if (settings->value(QStringLiteral("run_without_secure_secrets_service"), false).toBool()) { settings->remove("secrets/" + name_); // if we emit the signal directly it won't be received QTimer::singleShot(0, this, [this, name] { emit secretChanged(name); }); return; } auto job = new QKeychain::DeletePasswordJob(QCoreApplication::applicationName()); job->setAutoDelete(true); job->setInsecureFallback(true); job->setSettings(settings); job->setKey(name_); job->connect( job, &QKeychain::Job::finished, this, [this, name]() { emit secretChanged(name); }); job->start(); } std::string Cache::pickleSecret() { if (pickle_secret_.empty()) { this->pickle_secret_ = mtx::client::utils::random_token(64, true); storeSecretInStore("pickle_secret", pickle_secret_); } return pickle_secret_; } void Cache::storeEventExpirationProgress(const std::string &room, const std::string &expirationSettings, const std::string &stopMarker) { nlohmann::json j; j["s"] = expirationSettings; j["m"] = stopMarker; auto txn = lmdb::txn::begin(env_); eventExpiryBgJob_.put(txn, room, j.dump()); txn.commit(); } std::string Cache::loadEventExpirationProgress(const std::string &room, const std::string &expirationSettings) { try { auto txn = ro_txn(env_); std::string_view data; if (!eventExpiryBgJob_.get(txn, room, data)) return ""; auto j = nlohmann::json::parse(data); if (j.value("s", "") == expirationSettings) return j.value("m", ""); } catch (...) { return ""; } return ""; } void Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { nhlog::db()->info("mark room {} as encrypted", room_id); encryptedRooms_.put(txn, room_id, "0"); } bool Cache::isRoomEncrypted(const std::string &room_id) { std::string_view unused; auto txn = ro_txn(env_); auto res = encryptedRooms_.get(txn, room_id, unused); return res; } std::optional Cache::roomEncryptionSettings(const std::string &room_id) { using namespace mtx::events; using namespace mtx::events::state; try { auto txn = ro_txn(env_); auto statesdb = getStatesDb(txn, room_id); std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); return msg.content; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.encryption event: {}", e.what()); return Encryption{}; } } } catch (lmdb::error &) { } return std::nullopt; } mtx::crypto::ExportedSessionKeys Cache::exportSessionKeys() { using namespace mtx::crypto; ExportedSessionKeys keys; auto txn = ro_txn(env_); auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); std::string_view key, value; while (cursor.get(key, value, MDB_NEXT)) { ExportedSession exported; MegolmSessionIndex index; auto saved_session = unpickle(std::string(value), pickle_secret_); try { index = nlohmann::json::parse(key).get(); } catch (const nlohmann::json::exception &e) { nhlog::db()->critical("failed to export megolm session: {}", e.what()); continue; } try { using namespace mtx::crypto; std::string_view v; if (megolmSessionDataDb_.get(txn, nlohmann::json(index).dump(), v)) { auto data = nlohmann::json::parse(v).get(); exported.sender_key = data.sender_key; if (!data.sender_claimed_ed25519_key.empty()) exported.sender_claimed_keys["ed25519"] = data.sender_claimed_ed25519_key; exported.forwarding_curve25519_key_chain = data.forwarding_curve25519_key_chain; } else { continue; } } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what()); continue; } exported.room_id = index.room_id; exported.session_id = index.session_id; exported.session_key = export_session(saved_session.get(), -1); keys.sessions.push_back(exported); } cursor.close(); return keys; } void Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) { std::size_t importCount = 0; auto txn = lmdb::txn::begin(env_); for (const auto &s : keys.sessions) { MegolmSessionIndex index; index.room_id = s.room_id; index.session_id = s.session_id; GroupSessionData data{}; data.sender_key = s.sender_key; data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain; data.trusted = false; if (s.sender_claimed_keys.count("ed25519")) data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519"); try { auto exported_session = mtx::crypto::import_session(s.session_key); using namespace mtx::crypto; const auto key = nlohmann::json(index).dump(); const auto pickled = pickle(exported_session.get(), pickle_secret_); std::string_view value; if (inboundMegolmSessionDb_.get(txn, key, value)) { auto oldSession = unpickle(std::string(value), pickle_secret_); if (olm_inbound_group_session_first_known_index(exported_session.get()) >= olm_inbound_group_session_first_known_index(oldSession.get())) { nhlog::crypto()->warn( "Not storing inbound session with newer or equal first known index"); continue; } } inboundMegolmSessionDb_.put(txn, key, pickled); megolmSessionDataDb_.put(txn, key, nlohmann::json(data).dump()); ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); importCount++; } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to import inbound megolm session {}: {}", index.session_id, e.what()); continue; } catch (const lmdb::error &e) { nhlog::crypto()->critical( "failed to save inbound megolm session {}: {}", index.session_id, e.what()); continue; } } txn.commit(); nhlog::crypto()->info("Imported {} out of {} keys", importCount, keys.sessions.size()); } // // Session Management // void Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session, const GroupSessionData &data) { using namespace mtx::crypto; const auto key = nlohmann::json(index).dump(); const auto pickled = pickle(session.get(), pickle_secret_); auto txn = lmdb::txn::begin(env_); std::string_view value; if (inboundMegolmSessionDb_.get(txn, key, value)) { auto oldSession = unpickle(std::string(value), pickle_secret_); auto newIndex = olm_inbound_group_session_first_known_index(session.get()); auto oldIndex = olm_inbound_group_session_first_known_index(oldSession.get()); // merge trusted > untrusted // first known index minimum if (megolmSessionDataDb_.get(txn, key, value)) { auto oldData = nlohmann::json::parse(value).get(); if (oldData.trusted && newIndex >= oldIndex) { nhlog::crypto()->warn( "Not storing inbound session of lesser trust or bigger index."); return; } oldData.trusted = data.trusted || oldData.trusted; if (newIndex < oldIndex) { inboundMegolmSessionDb_.put(txn, key, pickled); oldData.message_index = newIndex; } megolmSessionDataDb_.put(txn, key, nlohmann::json(oldData).dump()); txn.commit(); return; } } inboundMegolmSessionDb_.put(txn, key, pickled); megolmSessionDataDb_.put(txn, key, nlohmann::json(data).dump()); txn.commit(); } mtx::crypto::InboundGroupSessionPtr Cache::getInboundMegolmSession(const MegolmSessionIndex &index) { using namespace mtx::crypto; try { auto txn = ro_txn(env_); std::string key = nlohmann::json(index).dump(); std::string_view value; if (inboundMegolmSessionDb_.get(txn, key, value)) { auto session = unpickle(std::string(value), pickle_secret_); return session; } } catch (std::exception &e) { nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); } return nullptr; } bool Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) { using namespace mtx::crypto; try { auto txn = ro_txn(env_); std::string key = nlohmann::json(index).dump(); std::string_view value; return inboundMegolmSessionDb_.get(txn, key, value); } catch (std::exception &e) { nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); } return false; } void Cache::updateOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &ptr) { using namespace mtx::crypto; if (!outboundMegolmSessionExists(room_id)) return; GroupSessionData data = data_; data.message_index = olm_outbound_group_session_message_index(ptr.get()); MegolmSessionIndex index; index.room_id = room_id; index.session_id = mtx::crypto::session_id(ptr.get()); // Save the updated pickled data for the session. nlohmann::json j; j["session"] = pickle(ptr.get(), pickle_secret_); auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.put(txn, room_id, j.dump()); megolmSessionDataDb_.put(txn, nlohmann::json(index).dump(), nlohmann::json(data).dump()); txn.commit(); } void Cache::dropOutboundMegolmSession(const std::string &room_id) { using namespace mtx::crypto; if (!outboundMegolmSessionExists(room_id)) return; { auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.del(txn, room_id); // don't delete session data, so that we can still share the session. txn.commit(); } } void Cache::saveOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &session) { using namespace mtx::crypto; const auto pickled = pickle(session.get(), pickle_secret_); GroupSessionData data = data_; data.message_index = olm_outbound_group_session_message_index(session.get()); MegolmSessionIndex index; index.room_id = room_id; index.session_id = mtx::crypto::session_id(session.get()); nlohmann::json j; j["session"] = pickled; auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.put(txn, room_id, j.dump()); megolmSessionDataDb_.put(txn, nlohmann::json(index).dump(), nlohmann::json(data).dump()); txn.commit(); } bool Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept { try { auto txn = ro_txn(env_); std::string_view value; return outboundMegolmSessionDb_.get(txn, room_id, value); } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); return false; } } OutboundGroupSessionDataRef Cache::getOutboundMegolmSession(const std::string &room_id) { try { using namespace mtx::crypto; auto txn = ro_txn(env_); std::string_view value; outboundMegolmSessionDb_.get(txn, room_id, value); auto obj = nlohmann::json::parse(value); OutboundGroupSessionDataRef ref{}; ref.session = unpickle(obj.at("session").get(), pickle_secret_); MegolmSessionIndex index; index.room_id = room_id; index.session_id = mtx::crypto::session_id(ref.session.get()); if (megolmSessionDataDb_.get(txn, nlohmann::json(index).dump(), value)) { ref.data = nlohmann::json::parse(value).get(); } return ref; } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); return {}; } } std::optional Cache::getMegolmSessionData(const MegolmSessionIndex &index) { try { using namespace mtx::crypto; auto txn = ro_txn(env_); std::string_view value; if (megolmSessionDataDb_.get(txn, nlohmann::json(index).dump(), value)) { return nlohmann::json::parse(value).get(); } return std::nullopt; } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what()); return std::nullopt; } } // // OLM sessions. // void Cache::saveOlmSessions(std::vector> sessions, uint64_t timestamp) { using namespace mtx::crypto; auto txn = lmdb::txn::begin(env_); for (const auto &[curve25519, session] : sessions) { const auto pickled = pickle(session.get(), pickle_secret_); const auto session_id = mtx::crypto::session_id(session.get()); StoredOlmSession stored_session; stored_session.pickled_session = pickled; stored_session.last_message_ts = timestamp; olmSessionDb_.put(txn, combineOlmSessionKeyFromCurveAndSessionId(curve25519, session_id), nlohmann::json(stored_session).dump()); } txn.commit(); } void Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session, uint64_t timestamp) { using namespace mtx::crypto; auto txn = lmdb::txn::begin(env_); const auto pickled = pickle(session.get(), pickle_secret_); const auto session_id = mtx::crypto::session_id(session.get()); StoredOlmSession stored_session; stored_session.pickled_session = pickled; stored_session.last_message_ts = timestamp; olmSessionDb_.put(txn, combineOlmSessionKeyFromCurveAndSessionId(curve25519, session_id), nlohmann::json(stored_session).dump()); txn.commit(); } std::optional Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) { using namespace mtx::crypto; try { auto txn = ro_txn(env_); std::string_view pickled; bool found = olmSessionDb_.get( txn, combineOlmSessionKeyFromCurveAndSessionId(curve25519, session_id), pickled); if (found) { auto data = nlohmann::json::parse(pickled).get(); return unpickle(data.pickled_session, pickle_secret_); } } catch (...) { } return std::nullopt; } std::optional Cache::getLatestOlmSession(const std::string &curve25519) { using namespace mtx::crypto; try { auto txn = ro_txn(env_); std::string_view key = curve25519, pickled_session; std::optional currentNewest; auto cursor = lmdb::cursor::open(txn, olmSessionDb_); bool first = true; while (cursor.get(key, pickled_session, first ? MDB_SET_RANGE : MDB_NEXT)) { first = false; auto storedCurve = splitCurve25519AndOlmSessionId(key).first; if (storedCurve != curve25519) break; auto data = nlohmann::json::parse(pickled_session).get(); if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts) currentNewest = data; } cursor.close(); return currentNewest ? std::optional(unpickle(currentNewest->pickled_session, pickle_secret_)) : std::nullopt; } catch (...) { return std::nullopt; } } std::vector Cache::getOlmSessions(const std::string &curve25519) { using namespace mtx::crypto; try { auto txn = ro_txn(env_); std::string_view key = curve25519, value; std::vector res; auto cursor = lmdb::cursor::open(txn, olmSessionDb_); bool first = true; while (cursor.get(key, value, first ? MDB_SET_RANGE : MDB_NEXT)) { first = false; auto [storedCurve, session_id] = splitCurve25519AndOlmSessionId(key); if (storedCurve != curve25519) break; res.emplace_back(session_id); } cursor.close(); return res; } catch (...) { return {}; } } void Cache::saveOlmAccount(const std::string &data) { auto txn = lmdb::txn::begin(env_); syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data); txn.commit(); } std::string Cache::restoreOlmAccount() { auto txn = ro_txn(env_); std::string_view pickled; syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled); return std::string(pickled.data(), pickled.size()); } void Cache::saveBackupVersion(const OnlineBackupVersion &data) { auto txn = lmdb::txn::begin(env_); syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump()); txn.commit(); } void Cache::deleteBackupVersion() { auto txn = lmdb::txn::begin(env_); syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION); txn.commit(); } std::optional Cache::backupVersion() { try { auto txn = ro_txn(env_); std::string_view v; syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v); return nlohmann::json::parse(v).get(); } catch (...) { return std::nullopt; } } void Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) { invitesDb_.del(txn, room_id); getInviteStatesDb(txn, room_id).drop(txn, true); getInviteMembersDb(txn, room_id).drop(txn, true); } void Cache::removeInvite(const std::string &room_id) { auto txn = lmdb::txn::begin(env_); removeInvite(txn, room_id); txn.commit(); } void Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) { roomsDb_.del(txn, roomid); getStatesDb(txn, roomid).drop(txn, true); getAccountDataDb(txn, roomid).drop(txn, true); getMembersDb(txn, roomid).drop(txn, true); } void Cache::removeRoom(const std::string &roomid) { auto txn = lmdb::txn::begin(env_, nullptr, 0); roomsDb_.del(txn, roomid); txn.commit(); } void Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) { syncStateDb_.put(txn, NEXT_BATCH_KEY, token); } bool Cache::isInitialized() { if (!env_.handle()) return false; auto txn = ro_txn(env_); std::string_view token; bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); return res; } std::string Cache::nextBatchToken() { if (!env_.handle()) throw lmdb::error("Env already closed", MDB_INVALID); auto txn = ro_txn(env_); std::string_view token; bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); if (result) return std::string(token.data(), token.size()); else return ""; } void Cache::deleteData() { if (this->databaseReady_) { this->databaseReady_ = false; // TODO: We need to remove the env_ while not accepting new requests. lmdb::dbi_close(env_, syncStateDb_); lmdb::dbi_close(env_, roomsDb_); lmdb::dbi_close(env_, invitesDb_); lmdb::dbi_close(env_, readReceiptsDb_); lmdb::dbi_close(env_, notificationsDb_); lmdb::dbi_close(env_, devicesDb_); lmdb::dbi_close(env_, deviceKeysDb_); lmdb::dbi_close(env_, inboundMegolmSessionDb_); lmdb::dbi_close(env_, outboundMegolmSessionDb_); lmdb::dbi_close(env_, megolmSessionDataDb_); env_.close(); verification_storage.status.clear(); if (!cacheDirectory_.isEmpty()) { QDir(cacheDirectory_).removeRecursively(); nhlog::db()->info("deleted cache files from disk"); } deleteSecretFromStore("pickle_secret", true); } } //! migrates db to the current format bool Cache::runMigrations() { std::string stored_version; { auto txn = ro_txn(env_); std::string_view current_version; bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version); if (!res) return false; stored_version = std::string(current_version); } std::vector>> migrations{ {"2020.05.01", [this]() { try { auto txn = lmdb::txn::begin(env_, nullptr); auto pending_receipts = lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); lmdb::dbi_drop(txn, pending_receipts, true); txn.commit(); } catch (const lmdb::error &) { nhlog::db()->critical("Failed to delete pending_receipts database in migration!"); return false; } 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); std::string_view 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 = nlohmann::json::parse( std::string_view(stored_message.data(), stored_message.size())); if (oldMessages.prev_batch.empty()) oldMessages.prev_batch = j["token"].get(); else if (j["token"].get() != oldMessages.prev_batch) break; oldMessages.events.push_back( j["event"].get()); } // 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 auto eventsDb = getEventsDb(txn, room_id); saveTimelineMessages(txn, eventsDb, 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; }}, {"2020.10.20", [this]() { try { using namespace mtx::crypto; auto txn = lmdb::txn::begin(env_); auto mainDb = lmdb::dbi::open(txn, nullptr); std::string_view dbName, ignored; auto olmDbCursor = lmdb::cursor::open(txn, mainDb); while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) { // skip every db but olm session dbs nhlog::db()->debug("Db {}", dbName); if (dbName.find("olm_sessions/") != 0) continue; nhlog::db()->debug("Migrating {}", dbName); auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str()); std::string_view session_id, session_value; std::vector> sessions; auto cursor = lmdb::cursor::open(txn, olmDb); while (cursor.get(session_id, session_value, MDB_NEXT)) { nhlog::db()->debug( "session_id {}, session_value {}", session_id, session_value); StoredOlmSession session; bool invalid = false; for (auto c : session_value) if (!isprint(c)) { invalid = true; break; } if (invalid) continue; nhlog::db()->debug("Not skipped"); session.pickled_session = session_value; sessions.emplace_back(session_id, session); } cursor.close(); olmDb.drop(txn, true); auto newDbName = std::string(dbName); newDbName.erase(0, sizeof("olm_sessions") - 1); newDbName = "olm_sessions.v2" + newDbName; auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE); for (const auto &[key, value] : sessions) { // nhlog::db()->debug("{}\n{}", key, nlohmann::json(value).dump()); newDb.put(txn, key, nlohmann::json(value).dump()); } } olmDbCursor.close(); txn.commit(); } catch (const lmdb::error &) { nhlog::db()->critical("Failed to migrate olm sessions,"); return false; } nhlog::db()->info("Successfully migrated olm sessions."); return true; }}, {"2021.08.22", [this]() { try { auto txn = lmdb::txn::begin(env_, nullptr); auto try_drop = [&txn](const std::string &dbName) { try { auto db = lmdb::dbi::open(txn, dbName.c_str()); db.drop(txn, true); } catch (std::exception &e) { nhlog::db()->warn("Failed to drop '{}': {}", dbName, e.what()); } }; auto room_ids = getRoomIds(txn); for (const auto &room : room_ids) { try_drop(room + "/state"); try_drop(room + "/state_by_key"); try_drop(room + "/account_data"); try_drop(room + "/members"); try_drop(room + "/mentions"); try_drop(room + "/events"); try_drop(room + "/event_order"); try_drop(room + "/event2order"); try_drop(room + "/msg2order"); try_drop(room + "/order2msg"); try_drop(room + "/pending"); try_drop(room + "/related"); } // clear db, don't delete roomsDb_.drop(txn, false); setNextBatchToken(txn, ""); txn.commit(); } catch (const lmdb::error &) { nhlog::db()->critical("Failed to clear cache!"); return false; } nhlog::db()->info("Successfully cleared the cache. Will do a clean sync after startup."); return true; }}, {"2021.08.31", [this]() { storeSecretInStore("pickle_secret", "secret"); this->pickle_secret_ = "secret"; return true; }}, {"2022.04.08", [this]() { try { auto txn = lmdb::txn::begin(env_, nullptr); auto inboundMegolmSessionDb = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); auto outboundMegolmSessionDb = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); auto megolmSessionDataDb = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); try { outboundMegolmSessionDb.drop(txn, false); } catch (std::exception &e) { nhlog::db()->warn("Failed to drop outbound sessions: {}", e.what()); } std::string_view key, value; auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb); std::map inboundSessions; std::map megolmSessionData; while (cursor.get(key, value, MDB_NEXT)) { auto indexVal = nlohmann::json::parse(key); if (!indexVal.contains("sender_key") || !indexVal.at("sender_key").is_string()) continue; auto sender_key = indexVal["sender_key"].get(); indexVal.erase("sender_key"); std::string_view dataVal; bool res = megolmSessionDataDb.get(txn, key, dataVal); if (res) { auto data = nlohmann::json::parse(dataVal); data["sender_key"] = sender_key; inboundSessions[indexVal.dump()] = std::string(value); megolmSessionData[indexVal.dump()] = data.dump(); } } cursor.close(); inboundMegolmSessionDb.drop(txn, false); megolmSessionDataDb.drop(txn, false); for (const auto &[k, v] : inboundSessions) { inboundMegolmSessionDb.put(txn, k, v); } for (const auto &[k, v] : megolmSessionData) { megolmSessionDataDb.put(txn, k, v); } txn.commit(); return true; } catch (std::exception &e) { nhlog::db()->warn( "Failed to migrate stored megolm session to have no sender key: {}", e.what()); return false; } }}, {"2022.11.06", [this]() { this->databaseReady_ = false; loadSecretsFromStore( { {std::string(mtx::secret_storage::secrets::cross_signing_master), false}, {std::string(mtx::secret_storage::secrets::cross_signing_self_signing), false}, {std::string(mtx::secret_storage::secrets::cross_signing_user_signing), false}, {std::string(mtx::secret_storage::secrets::megolm_backup_v1), false}, }, [this, count = 1](const std::string &name, bool internal, const std::string &value) mutable { nhlog::db()->critical("Loaded secret {}", name); this->storeSecret(name, value, internal); // HACK(Nico): delay deletion to not crash because of multiple nested deletions. // Since this is just migration code, this should be *fine*. QTimer::singleShot(count * 2000, this, [this, name, internal] { deleteSecretFromStore(name, internal); }); count++; }, false); while (!this->databaseReady_) { QCoreApplication::instance()->processEvents(QEventLoop::AllEvents, 100); } return true; }}, {"2023.03.12", [this]() { try { auto txn = lmdb::txn::begin(env_, nullptr); auto room_ids = getRoomIds(txn); for (const auto &room_id : room_ids) { try { auto oldStateskeyDb = lmdb::dbi::open(txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT); lmdb::dbi_set_dupsort( txn, oldStateskeyDb, +[](const MDB_val *a, const MDB_val *b) { auto get_skey = [](const MDB_val *v) { return nlohmann::json::parse( std::string_view(static_cast(v->mv_data), v->mv_size)) .value("key", ""); }; return get_skey(a).compare(get_skey(b)); }); auto newStateskeyDb = getStatesKeyDb(txn, room_id); // convert the dupsort format { auto cursor = lmdb::cursor::open(txn, oldStateskeyDb); std::string_view ev_type, data; bool start = true; while (cursor.get(ev_type, data, start ? MDB_FIRST : MDB_NEXT)) { start = false; auto j = nlohmann::json::parse(std::string_view(data.data(), data.size())); newStateskeyDb.put( txn, ev_type, j.value("key", "") + '\0' + j.value("id", "")); } } // delete old db lmdb::dbi_drop(txn, oldStateskeyDb, true); } catch (std::exception &e) { nhlog::db()->error("While migrating state events from {}, ignoring error {}", room_id, e.what()); } } txn.commit(); } catch (const lmdb::error &) { nhlog::db()->critical("Failed to convert states key database in migration!"); return false; } nhlog::db()->info("Successfully updated states key database format."); return true; }}, {"2023.10.22", [this]() { // migrate olm sessions to a single db try { auto txn = lmdb::txn::begin(env_, nullptr); auto mainDb = lmdb::dbi::open(txn); auto dbNames = lmdb::cursor::open(txn, mainDb); bool doCommit = false; std::string_view dbName; while (dbNames.get(dbName, MDB_NEXT)) { if (!dbName.starts_with("olm_sessions.v2/")) continue; doCommit = true; auto curveKey = dbName; curveKey.remove_prefix(std::string_view("olm_sessions.v2/").size()); auto oldDb = lmdb::dbi::open(txn, std::string(dbName).c_str()); auto olmCursor = lmdb::cursor::open(txn, oldDb); std::string_view session_id, json; while (olmCursor.get(session_id, json, MDB_NEXT)) { olmSessionDb_.put( txn, combineOlmSessionKeyFromCurveAndSessionId(curveKey, session_id), json); } olmCursor.close(); oldDb.drop(txn, true); } dbNames.close(); if (doCommit) txn.commit(); } catch (const lmdb::error &e) { nhlog::db()->critical("Failed to convert olm sessions database in migration! {}", e.what()); return false; } nhlog::db()->info("Successfully updated olm sessions database format."); 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()) { nhlog::db()->critical("migration failure!"); return false; } } nhlog::db()->info("Migrations finished."); setCurrentFormat(); return true; } cache::CacheVersion Cache::formatVersion() { auto txn = ro_txn(env_); std::string_view current_version; bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version); if (!res) return cache::CacheVersion::Older; std::string stored_version(current_version.data(), current_version.size()); if (stored_version < CURRENT_CACHE_FORMAT_VERSION) return cache::CacheVersion::Older; else if (stored_version > CURRENT_CACHE_FORMAT_VERSION) return cache::CacheVersion::Older; else return cache::CacheVersion::Current; } void Cache::setCurrentFormat() { auto txn = lmdb::txn::begin(env_); syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION); txn.commit(); } CachedReceipts Cache::readReceipts(const QString &event_id, const QString &room_id) { CachedReceipts receipts; ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()}; nlohmann::json json_key = receipt_key; try { auto txn = ro_txn(env_); auto key = json_key.dump(); std::string_view value; bool res = readReceiptsDb_.get(txn, key, value); if (res) { auto json_response = nlohmann::json::parse(std::string_view(value.data(), value.size())); auto values = json_response.get>(); for (const auto &v : values) // timestamp, user_id receipts.emplace(v.second, v.first); } } catch (const lmdb::error &e) { nhlog::db()->critical("readReceipts: {}", e.what()); } return receipts; } void Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) { auto user_id = this->localUserId_.toStdString(); for (const auto &receipt : receipts) { const auto event_id = receipt.first; auto event_receipts = receipt.second; ReadReceiptKey receipt_key{event_id, room_id}; nlohmann::json json_key = receipt_key; try { const auto key = json_key.dump(); std::string_view prev_value; bool exists = readReceiptsDb_.get(txn, key, prev_value); std::map saved_receipts; // If an entry for the event id already exists, we would // merge the existing receipts with the new ones. if (exists) { auto json_value = nlohmann::json::parse(std::string_view(prev_value.data(), prev_value.size())); // Retrieve the saved receipts. saved_receipts = json_value.get>(); } // Append the new ones. for (const auto &[read_by, timestamp] : event_receipts) { saved_receipts.emplace(read_by, timestamp); } // Save back the merged (or only the new) receipts. nlohmann::json json_updated_value = saved_receipts; std::string merged_receipts = json_updated_value.dump(); readReceiptsDb_.put(txn, key, merged_receipts); } catch (const lmdb::error &e) { nhlog::db()->critical("updateReadReceipts: {}", e.what()); } } } std::string Cache::getFullyReadEventId(const std::string &room_id) { auto txn = ro_txn(env_); if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) { if (auto fr = std::get_if>( &ev.value())) { return fr->content.event_id; } } return std::string(); } void Cache::calculateRoomReadStatus() { const auto joined_rooms = joinedRooms(); std::map readStatus; for (const auto &room : joined_rooms) readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room)); emit roomReadStatus(readStatus); } bool Cache::calculateRoomReadStatus(const std::string &room_id) { std::string last_event_id_, fullyReadEventId_; { auto txn = ro_txn(env_); // Get last event id on the room. const auto last_event_id = getLastEventId(txn, room_id); const auto localUser = utils::localUser().toStdString(); std::string fullyReadEventId = getFullyReadEventId(room_id); if (last_event_id.empty() || fullyReadEventId.empty()) return true; if (last_event_id == fullyReadEventId) return false; last_event_id_ = std::string(last_event_id); fullyReadEventId_ = std::string(fullyReadEventId); } // Retrieve all read receipts for that event. return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_); } void Cache::updateState(const std::string &room, const mtx::responses::StateEvents &state, bool wipe) { auto txn = lmdb::txn::begin(env_); auto statesdb = getStatesDb(txn, room); auto stateskeydb = getStatesKeyDb(txn, room); auto membersdb = getMembersDb(txn, room); auto eventsDb = getEventsDb(txn, room); if (wipe) { membersdb.drop(txn); statesdb.drop(txn); stateskeydb.drop(txn); } saveStateEvents(txn, statesdb, stateskeydb, membersdb, eventsDb, room, state.events); RoomInfo updatedInfo; { std::string_view data; if (roomsDb_.get(txn, room, data)) { try { updatedInfo = nlohmann::json::parse(std::string_view(data.data(), data.size())).get(); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", room, std::string(data.data(), data.size()), e.what()); } } } updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); updatedInfo.is_space = getRoomIsSpace(txn, statesdb); updatedInfo.is_tombstoned = getRoomIsTombstoned(txn, statesdb); roomsDb_.put(txn, room, nlohmann::json(updatedInfo).dump()); updateSpaces(txn, {room}, {room}); txn.commit(); } template std::optional> Cache::getStateEvent(lmdb::txn &txn, const std::string &room_id, std::string_view state_key) { try { constexpr auto type = mtx::events::state_content_to_type; static_assert(type != mtx::events::EventType::Unsupported, "Not a supported type in state events."); if (room_id.empty()) return std::nullopt; const auto typeStr = to_string(type); std::string_view value; if (state_key.empty()) { auto db = getStatesDb(txn, room_id); if (!db.get(txn, typeStr, value)) { return std::nullopt; } } else { auto db = getStatesKeyDb(txn, room_id); // we can search using state key, since the compare functions defaults to the whole // string, when there is no nullbyte std::string_view data = state_key; std::string_view typeStrV = typeStr; auto cursor = lmdb::cursor::open(txn, db); if (!cursor.get(typeStrV, data, MDB_GET_BOTH)) return std::nullopt; try { auto eventsDb = getEventsDb(txn, room_id); auto eventid = data; if (auto sep = data.rfind('\0'); sep != std::string_view::npos) { if (!eventsDb.get(txn, eventid.substr(sep + 1), value)) return std::nullopt; } else { return std::nullopt; } } catch (std::exception &) { return std::nullopt; } } return nlohmann::json::parse(value).get>(); } catch (std::exception &) { return std::nullopt; } } template std::vector> Cache::getStateEventsWithType(lmdb::txn &txn, const std::string &room_id, mtx::events::EventType type) { if (room_id.empty()) return {}; std::vector> events; { auto db = getStatesKeyDb(txn, room_id); auto eventsDb = getEventsDb(txn, room_id); const auto typeStr = to_string(type); std::string_view typeStrV = typeStr; std::string_view data; std::string_view value; auto cursor = lmdb::cursor::open(txn, db); bool first = true; if (cursor.get(typeStrV, data, MDB_SET)) { while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { first = false; try { auto eventid = data; if (auto sep = data.rfind('\0'); sep != std::string_view::npos) { if (eventsDb.get(txn, eventid.substr(sep + 1), value)) events.push_back( nlohmann::json::parse(value).get>()); } } catch (std::exception &e) { nhlog::db()->warn("Failed to parse state event: {}", e.what()); } } } } return events; } template void Cache::saveStateEvents(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &stateskeydb, lmdb::dbi &membersdb, lmdb::dbi &eventsDb, const std::string &room_id, const std::vector &events) { for (const auto &e : events) saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e); } template void Cache::saveStateEvent(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &stateskeydb, lmdb::dbi &membersdb, lmdb::dbi &eventsDb, const std::string &room_id, const T &event) { using namespace mtx::events; using namespace mtx::events::state; if (auto e = std::get_if>(&event); e != nullptr) { switch (e->content.membership) { // // We only keep users with invite or join membership. // case Membership::Invite: case Membership::Join: { auto display_name = e->content.display_name.empty() ? e->state_key : e->content.display_name; std::string inviter = ""; if (e->content.membership == mtx::events::state::Membership::Invite) { inviter = e->sender; } // Lightweight representation of a member. MemberInfo tmp{ display_name, e->content.avatar_url, inviter, e->content.reason, e->content.is_direct, }; membersdb.put(txn, e->state_key, nlohmann::json(tmp).dump()); break; } default: { membersdb.del(txn, e->state_key, ""); break; } } } else if (auto encr = std::get_if>(&event)) { if (!encr->state_key.empty()) return; setEncryptedRoom(txn, room_id); std::string_view temp; // ensure we don't replace the event in the db if (statesdb.get(txn, to_string(encr->type), temp)) { return; } } std::visit( [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) { if constexpr (isStateEvent_) { eventsDb.put(txn, e.event_id, nlohmann::json(e).dump()); if (e.type != EventType::Unsupported) { if (std::is_same_v>, StateEvent>) { // apply the redaction event if (e.type == EventType::RoomMember) { // membership is not revoked, but names are yeeted (so we set the name // to the mxid) MemberInfo tmp{e.state_key, ""}; membersdb.put(txn, e.state_key, nlohmann::json(tmp).dump()); } else if (e.state_key.empty()) { // strictly speaking some stuff in those events can be redacted, but // this is close enough. Ref: // https://spec.matrix.org/v1.6/rooms/v10/#redactions if (e.type != EventType::RoomCreate && e.type != EventType::RoomJoinRules && e.type != EventType::RoomPowerLevels && e.type != EventType::RoomHistoryVisibility) statesdb.del(txn, to_string(e.type)); } else stateskeydb.del(txn, to_string(e.type), e.state_key + '\0' + e.event_id); } else if (e.state_key.empty()) { statesdb.put(txn, to_string(e.type), nlohmann::json(e).dump()); } else { auto data = e.state_key + '\0' + e.event_id; auto key = to_string(e.type); // Work around https://bugs.openldap.org/show_bug.cgi?id=8447 stateskeydb.del(txn, key, data); stateskeydb.put(txn, key, data); } } } }, event); } void Cache::saveState(const mtx::responses::Sync &res) try { using namespace mtx::events; auto local_user_id = this->localUserId_.toStdString(); auto currentBatchToken = res.next_batch; auto txn = lmdb::txn::begin(env_); setNextBatchToken(txn, res.next_batch); if (!res.account_data.events.empty()) { auto accountDataDb = getAccountDataDb(txn, ""); for (const auto &ev : res.account_data.events) std::visit( [&txn, &accountDataDb](const auto &event) { if constexpr (std::is_same_v< std::remove_cv_t>, AccountDataEvent< mtx::events::account_data::nheko_extensions::HiddenEvents>>) { if (!event.content.hidden_event_types) { accountDataDb.del(txn, "im.nheko.hidden_events"); return; } } auto j = nlohmann::json(event); accountDataDb.put(txn, j["type"].get(), j.dump()); }, ev); } auto userKeyCacheDb = getUserKeysDb(txn); std::set spaces_with_updates; std::set rooms_with_space_updates; // Save joined rooms for (const auto &room : res.rooms.join) { auto statesdb = getStatesDb(txn, room.first); auto stateskeydb = getStatesKeyDb(txn, room.first); auto membersdb = getMembersDb(txn, room.first); auto eventsDb = getEventsDb(txn, room.first); // nhlog::db()->critical( // "Saving events for room: {}, state {}, timeline {}, account {}, ephemeral {}", // room.first, // room.second.state.events.size(), // room.second.timeline.events.size(), // room.second.account_data.events.size(), // room.second.ephemeral.events.size()); saveStateEvents( txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.state.events); saveStateEvents( txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.timeline.events); saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline); RoomInfo updatedInfo; std::string_view originalRoomInfoDump; { // retrieve the old tags and modification ts if (roomsDb_.get(txn, room.first, originalRoomInfoDump)) { try { RoomInfo tmp = nlohmann::json::parse(originalRoomInfoDump).get(); updatedInfo.tags = std::move(tmp.tags); updatedInfo.approximate_last_modification_ts = tmp.approximate_last_modification_ts; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", room.first, originalRoomInfoDump, e.what()); } } } updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); updatedInfo.is_space = getRoomIsSpace(txn, statesdb); updatedInfo.notification_count = room.second.unread_notifications.notification_count; updatedInfo.highlight_count = room.second.unread_notifications.highlight_count; if (updatedInfo.is_space) { bool space_updates = false; for (const auto &e : room.second.state.events) if (std::holds_alternative>(e) || std::holds_alternative>(e)) space_updates = true; for (const auto &e : room.second.timeline.events) if (std::holds_alternative>(e) || std::holds_alternative>(e)) space_updates = true; if (space_updates) spaces_with_updates.insert(room.first); } { bool room_has_space_update = false; for (const auto &e : room.second.state.events) { if (auto se = std::get_if>(&e)) { if (se->state_key.empty()) { nhlog::db()->warn("Skipping space parent with empty state key in room {}", room.first); } else { spaces_with_updates.insert(se->state_key); room_has_space_update = true; } } } for (const auto &e : room.second.timeline.events) { if (auto se = std::get_if>(&e)) { if (se->state_key.empty()) { nhlog::db()->warn("Skipping space child with empty state key in room {}", room.first); } else { spaces_with_updates.insert(se->state_key); room_has_space_update = true; } } } if (room_has_space_update) rooms_with_space_updates.insert(room.first); } // Process the account_data associated with this room if (!room.second.account_data.events.empty()) { auto accountDataDb = getAccountDataDb(txn, room.first); for (const auto &evt : room.second.account_data.events) { std::visit( [&txn, &accountDataDb](const auto &event) { if constexpr (std::is_same_v< std::remove_cv_t>, AccountDataEvent>) { if (!event.content.hidden_event_types) { accountDataDb.del(txn, "im.nheko.hidden_events"); return; } } auto j = nlohmann::json(event); accountDataDb.put(txn, j["type"].get(), j.dump()); }, evt); // for tag events if (std::holds_alternative>(evt)) { auto tags_evt = std::get>(evt); updatedInfo.tags.clear(); for (const auto &tag : tags_evt.content.tags) { updatedInfo.tags.push_back(tag.first); } } } } for (const auto &e : room.second.timeline.events) { if (!mtx::accessors::is_message(e)) continue; updatedInfo.approximate_last_modification_ts = mtx::accessors::origin_server_ts_ms(e); } if (auto newRoomInfoDump = nlohmann::json(updatedInfo).dump(); newRoomInfoDump != originalRoomInfoDump) { // nhlog::db()->critical( // "Writing out new room info:\n{}\n{}", originalRoomInfoDump, newRoomInfoDump); roomsDb_.put(txn, room.first, newRoomInfoDump); } for (const auto &e : room.second.ephemeral.events) { if (auto receiptsEv = std::get_if>(&e)) { Receipts receipts; for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) { if (auto r = userReceipts.find(mtx::events::ephemeral::Receipt::Read); r != userReceipts.end()) { for (const auto &[user_id, receipt] : r->second.users) { receipts[event_id][user_id] = receipt.ts; } } if (userReceipts.count(mtx::events::ephemeral::Receipt::ReadPrivate)) { const auto &users = userReceipts.at(mtx::events::ephemeral::Receipt::ReadPrivate).users; if (auto ts = users.find(local_user_id); ts != users.end() && ts->second.ts != 0) receipts[event_id][local_user_id] = ts->second.ts; } } updateReadReceipt(txn, room.first, receipts); } } // Clean up non-valid invites. removeInvite(txn, room.first); } saveInvites(txn, res.rooms.invite); savePresence(txn, res.presence); markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken); removeLeftRooms(txn, res.rooms.leave); updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates)); txn.commit(); std::map readStatus; for (const auto &room : res.rooms.join) { for (const auto &e : room.second.ephemeral.events) { if (auto receiptsEv = std::get_if>(&e)) { std::vector receipts; for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) { if (auto r = userReceipts.find(mtx::events::ephemeral::Receipt::Read); r != userReceipts.end()) { for (const auto &[user_id, receipt] : r->second.users) { (void)receipt; if (user_id != local_user_id) { receipts.push_back(QString::fromStdString(event_id)); break; } } } } if (!receipts.empty()) emit newReadReceipts(QString::fromStdString(room.first), receipts); } } readStatus.emplace(QString::fromStdString(room.first), calculateRoomReadStatus(room.first)); } emit roomReadStatus(readStatus); } catch (const lmdb::error &lmdbException) { if (lmdbException.code() == MDB_DBS_FULL || lmdbException.code() == MDB_MAP_FULL) { if (lmdbException.code() == MDB_DBS_FULL) { auto settings = UserSettings::instance(); unsigned roomDbCount = static_cast((res.rooms.invite.size() + res.rooms.join.size() + res.rooms.knock.size() + res.rooms.leave.size()) * 20); settings->qsettings()->setValue( MAX_DBS_SETTINGS_KEY, std::max( settings->qsettings()->value(MAX_DBS_SETTINGS_KEY, MAX_DBS_DEFAULT).toUInt() * 2, roomDbCount)); } else if (lmdbException.code() == MDB_MAP_FULL) { auto settings = UserSettings::instance(); MDB_envinfo envinfo = {}; lmdb::env_info(env_, &envinfo); settings->qsettings()->setValue(MAX_DB_SIZE_SETTINGS_KEY, static_cast(envinfo.me_mapsize * 2)); } QMessageBox::warning( nullptr, tr("Database limit reached"), tr("Your account is larger than our default database limit. We have " "increased the capacity automatically, however you will need to " "restart to apply this change. Nheko will now close automatically."), QMessageBox::StandardButton::Close); QCoreApplication::exit(1); exit(1); } throw; } void Cache::saveInvites(lmdb::txn &txn, const std::map &rooms) { for (const auto &room : rooms) { auto statesdb = getInviteStatesDb(txn, room.first); auto membersdb = getInviteMembersDb(txn, room.first); saveInvite(txn, statesdb, membersdb, room.second); RoomInfo updatedInfo; updatedInfo.name = getInviteRoomName(txn, statesdb, membersdb).toStdString(); updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); updatedInfo.avatar_url = getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); updatedInfo.is_space = getInviteRoomIsSpace(txn, statesdb); updatedInfo.is_invite = true; invitesDb_.put(txn, room.first, nlohmann::json(updatedInfo).dump()); } } void Cache::saveInvite(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const mtx::responses::InvitedRoom &room) { using namespace mtx::events; using namespace mtx::events::state; for (const auto &e : room.invite_state) { if (auto msg = std::get_if>(&e)) { auto display_name = msg->content.display_name.empty() ? msg->state_key : msg->content.display_name; std::string inviter = ""; if (msg->content.membership == mtx::events::state::Membership::Invite) { inviter = msg->sender; } MemberInfo tmp{display_name, msg->content.avatar_url, inviter, msg->content.reason, msg->content.is_direct}; membersdb.put(txn, msg->state_key, nlohmann::json(tmp).dump()); } else { std::visit( [&txn, &statesdb](auto msg) { auto j = nlohmann::json(msg); bool res = statesdb.put(txn, j["type"].get(), j.dump()); if (!res) nhlog::db()->warn("couldn't save data: {}", nlohmann::json(msg).dump()); }, e); } } } void Cache::savePresence( lmdb::txn &txn, const std::vector> &presenceUpdates) { for (const auto &update : presenceUpdates) { auto toWrite = nlohmann::json(update.content); // Nheko currently doesn't use those and it causes lots of db writes :) toWrite.erase("currently_active"); toWrite.erase("last_active_ago"); auto toWriteStr = toWrite.dump(); std::string_view oldPresenceVal; presenceDb_.get(txn, update.sender, oldPresenceVal); if (oldPresenceVal != toWriteStr) { // nhlog::db()->critical( // "Presence update for {}: {} -> {}", update.sender, oldPresenceVal, toWriteStr); presenceDb_.put(txn, update.sender, toWriteStr); } } } std::vector Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) { std::vector rooms; for (const auto &room : res.rooms.join) { bool hasUpdates = false; for (const auto &s : room.second.state.events) { if (mtx::accessors::is_state_event(s)) { hasUpdates = true; break; } } for (const auto &s : room.second.timeline.events) { if (mtx::accessors::is_state_event(s)) { hasUpdates = true; break; } } if (hasUpdates) rooms.emplace_back(room.first); } for (const auto &room : res.rooms.invite) { for (const auto &s : room.second.invite_state) { if (containsStateUpdates(s)) { rooms.emplace_back(room.first); break; } } } return rooms; } RoomInfo Cache::singleRoomInfo(const std::string &room_id) { auto txn = ro_txn(env_); try { auto statesdb = getStatesDb(txn, room_id); std::string_view data; // Check if the room is joined. if (roomsDb_.get(txn, room_id, data)) { try { RoomInfo tmp = nlohmann::json::parse(data).get(); tmp.member_count = getMembersDb(txn, room_id).size(txn); tmp.join_rule = getRoomJoinRule(txn, statesdb); tmp.guest_access = getRoomGuestAccess(txn, statesdb); return tmp; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", room_id, std::string(data.data(), data.size()), e.what()); } } } catch (const lmdb::error &e) { nhlog::db()->warn("failed to read room info from db: room_id ({}), {}", room_id, e.what()); } return RoomInfo(); } void Cache::updateLastMessageTimestamp(const std::string &room_id, uint64_t ts) { auto txn = lmdb::txn::begin(env_); try { auto statesdb = getStatesDb(txn, room_id); std::string_view data; // Check if the room is joined. if (roomsDb_.get(txn, room_id, data)) { try { RoomInfo tmp = nlohmann::json::parse(data).get(); tmp.approximate_last_modification_ts = ts; roomsDb_.put(txn, room_id, nlohmann::json(tmp).dump()); txn.commit(); return; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", room_id, std::string(data.data(), data.size()), e.what()); } } } catch (const lmdb::error &e) { nhlog::db()->warn("failed to read room info from db: room_id ({}), {}", room_id, e.what()); } } std::map Cache::getRoomInfo(const std::vector &rooms) { std::map room_info; // TODO This should be read only. auto txn = lmdb::txn::begin(env_); for (const auto &room : rooms) { std::string_view data; auto statesdb = getStatesDb(txn, room); // Check if the room is joined. if (roomsDb_.get(txn, room, data)) { try { RoomInfo tmp = nlohmann::json::parse(data).get(); tmp.member_count = getMembersDb(txn, room).size(txn); tmp.join_rule = getRoomJoinRule(txn, statesdb); tmp.guest_access = getRoomGuestAccess(txn, statesdb); room_info.emplace(QString::fromStdString(room), std::move(tmp)); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", room, std::string(data.data(), data.size()), e.what()); } } else { // Check if the room is an invite. if (invitesDb_.get(txn, room, data)) { try { RoomInfo tmp = nlohmann::json::parse(std::string_view(data)).get(); tmp.member_count = getInviteMembersDb(txn, room).size(txn); room_info.emplace(QString::fromStdString(room), std::move(tmp)); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info for invite: " "room_id ({}), {}: {}", room, std::string(data.data(), data.size()), e.what()); } } } } txn.commit(); return room_info; } std::vector Cache::roomIds() { auto txn = ro_txn(env_); std::vector rooms; std::string_view room_id, unused; auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); while (roomsCursor.get(room_id, unused, MDB_NEXT)) rooms.push_back(QString::fromStdString(std::string(room_id))); roomsCursor.close(); return rooms; } std::string Cache::previousBatchToken(const std::string &room_id) { auto txn = ro_txn(env_); try { auto orderDb = getEventOrderDb(txn, room_id); auto cursor = lmdb::cursor::open(txn, orderDb); std::string_view indexVal, val; if (!cursor.get(indexVal, val, MDB_FIRST)) { return ""; } auto j = nlohmann::json::parse(val); return j.value("prev_batch", ""); } catch (...) { return ""; } } std::optional Cache::getEvent(const std::string &room_id, std::string_view event_id) { auto txn = ro_txn(env_); auto eventsDb = getEventsDb(txn, room_id); std::string_view event{}; bool success = eventsDb.get(txn, event_id, event); if (!success) return {}; try { return nlohmann::json::parse(event).get(); } catch (std::exception &e) { nhlog::db()->error("Failed to parse message from cache {}", e.what()); return std::nullopt; } } void Cache::storeEvent(const std::string &room_id, const std::string &event_id, const mtx::events::collections::TimelineEvents &event) { auto txn = lmdb::txn::begin(env_); auto eventsDb = getEventsDb(txn, room_id); auto event_json = mtx::accessors::serialize_event(event); eventsDb.put(txn, event_id, event_json.dump()); txn.commit(); } void Cache::replaceEvent(const std::string &room_id, const std::string &event_id, const mtx::events::collections::TimelineEvents &event) { auto txn = lmdb::txn::begin(env_); auto eventsDb = getEventsDb(txn, room_id); auto relationsDb = getRelationsDb(txn, room_id); auto event_json = mtx::accessors::serialize_event(event).dump(); { eventsDb.del(txn, event_id); eventsDb.put(txn, event_id, event_json); for (const auto &relation : mtx::accessors::relations(event).relations) { relationsDb.put(txn, relation.event_id, event_id); } } txn.commit(); } std::vector Cache::relatedEvents(const std::string &room_id, const std::string &event_id) { auto txn = ro_txn(env_); auto relationsDb = getRelationsDb(txn, room_id); std::vector related_ids; auto related_cursor = lmdb::cursor::open(txn, relationsDb); std::string_view 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; } size_t Cache::memberCount(const std::string &room_id) { auto txn = ro_txn(env_); return getMembersDb(txn, room_id).size(txn); } QMap Cache::roomInfo(bool withInvites) { QMap result; auto txn = ro_txn(env_); std::string_view room_id; std::string_view room_data; // Gather info about the joined rooms. auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { RoomInfo tmp = nlohmann::json::parse(std::move(room_data)).get(); tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn); result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); } roomsCursor.close(); if (withInvites) { // Gather info about the invites. auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { RoomInfo tmp = nlohmann::json::parse(room_data).get(); tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); } invitesCursor.close(); } return result; } std::vector Cache::roomNamesAndAliases() { auto txn = ro_txn(env_); std::vector result; result.reserve(roomsDb_.size(txn)); std::string_view room_id; std::string_view room_data; auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { try { std::string room_id_str = std::string(room_id); RoomInfo info = nlohmann::json::parse(std::move(room_data)).get(); auto aliases = getStateEvent(txn, room_id_str); std::string alias; if (aliases) { alias = aliases->content.alias; } result.push_back(RoomNameAlias{ .id = std::move(room_id_str), .name = std::move(info.name), .alias = std::move(alias), .recent_activity = info.approximate_last_modification_ts, .is_tombstoned = info.is_tombstoned, .is_space = info.is_space, }); } catch (std::exception &e) { nhlog::db()->warn("Failed to add room {} to result: {}", room_id, e.what()); } } return result; } std::string Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id) { lmdb::dbi orderDb; 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 {}; } std::string_view indexVal, val; auto cursor = lmdb::cursor::open(txn, orderDb); if (!cursor.get(indexVal, val, MDB_LAST)) { return {}; } return std::string(val.data(), val.size()); } std::optional Cache::getTimelineRange(const std::string &room_id) { auto txn = ro_txn(env_); lmdb::dbi orderDb; 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 {}; } std::string_view indexVal, val; auto cursor = lmdb::cursor::open(txn, orderDb); if (!cursor.get(indexVal, val, MDB_LAST)) { return {}; } TimelineRange range{}; range.last = lmdb::from_sv(indexVal); if (!cursor.get(indexVal, val, MDB_FIRST)) { return {}; } range.first = lmdb::from_sv(indexVal); return range; } std::optional Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) { if (event_id.empty() || room_id.empty()) return {}; auto txn = ro_txn(env_); lmdb::dbi orderDb; try { orderDb = getMessageToOrderDb(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 {}; } std::string_view indexVal{event_id.data(), event_id.size()}, val; bool success = orderDb.get(txn, indexVal, val); if (!success) { return {}; } return lmdb::from_sv(val); } std::optional Cache::getEventIndex(const std::string &room_id, std::string_view event_id) { if (room_id.empty() || event_id.empty()) return {}; auto txn = ro_txn(env_); lmdb::dbi orderDb; try { orderDb = getEventToOrderDb(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 {}; } std::string_view val; bool success = orderDb.get(txn, event_id, val); if (!success) { return {}; } return lmdb::from_sv(val); } std::optional> Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) { if (room_id.empty() || event_id.empty()) return {}; auto txn = ro_txn(env_); lmdb::dbi orderDb; lmdb::dbi eventOrderDb; lmdb::dbi timelineDb; try { orderDb = getEventToOrderDb(txn, room_id); eventOrderDb = getEventOrderDb(txn, room_id); timelineDb = getMessageToOrderDb(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 {}; } std::string_view indexVal; bool success = orderDb.get(txn, event_id, indexVal); if (!success) { return {}; } try { uint64_t prevIdx = lmdb::from_sv(indexVal); std::string prevId{event_id}; auto cursor = lmdb::cursor::open(txn, eventOrderDb); cursor.get(indexVal, MDB_SET); while (cursor.get(indexVal, event_id, MDB_NEXT)) { std::string evId = nlohmann::json::parse(event_id)["event_id"].get(); std::string_view temp; if (timelineDb.get(txn, evId, temp)) { return std::pair{prevIdx, std::string(prevId)}; } else { prevIdx = lmdb::from_sv(indexVal); prevId = std::move(evId); } } return std::pair{prevIdx, std::string(prevId)}; } catch (lmdb::runtime_error &e) { nhlog::db()->error("Failed to get last invisible event after {}", event_id, e.what()); return {}; } } std::optional> Cache::lastVisibleEvent(const std::string &room_id, std::string_view event_id) { if (room_id.empty() || event_id.empty()) return {}; auto txn = ro_txn(env_); lmdb::dbi orderDb; lmdb::dbi eventOrderDb; lmdb::dbi timelineDb; try { orderDb = getEventToOrderDb(txn, room_id); eventOrderDb = getEventOrderDb(txn, room_id); timelineDb = getMessageToOrderDb(txn, room_id); std::string_view indexVal; bool success = orderDb.get(txn, event_id, indexVal); if (!success) { return {}; } uint64_t idx = lmdb::from_sv(indexVal); std::string evId{event_id}; auto cursor = lmdb::cursor::open(txn, eventOrderDb); if (cursor.get(indexVal, event_id, MDB_SET)) { do { evId = nlohmann::json::parse(event_id)["event_id"].get(); std::string_view temp; idx = lmdb::from_sv(indexVal); if (timelineDb.get(txn, evId, temp)) { return std::pair{idx, evId}; } } while (cursor.get(indexVal, event_id, MDB_PREV)); } return std::pair{idx, evId}; } catch (lmdb::runtime_error &e) { nhlog::db()->error("Failed to get last visible event after {}", event_id, e.what()); return {}; } } std::optional Cache::getTimelineEventId(const std::string &room_id, uint64_t index) { auto txn = ro_txn(env_); lmdb::dbi orderDb; 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 {}; } std::string_view val; bool success = orderDb.get(txn, lmdb::to_sv(index), val); if (!success) { return {}; } return std::string(val); } QHash Cache::invites() { QHash result; auto txn = ro_txn(env_); auto cursor = lmdb::cursor::open(txn, invitesDb_); std::string_view room_id, room_data; while (cursor.get(room_id, room_data, MDB_NEXT)) { try { RoomInfo tmp = nlohmann::json::parse(room_data).get(); tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info for invite: " "room_id ({}), {}: {}", room_id, std::string(room_data), e.what()); } } cursor.close(); return result; } std::optional Cache::invite(std::string_view roomid) { std::optional result; auto txn = ro_txn(env_); std::string_view room_data; if (invitesDb_.get(txn, roomid, room_data)) { try { RoomInfo tmp = nlohmann::json::parse(room_data).get(); tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn); result = std::move(tmp); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse room info for invite: " "room_id ({}), {}: {}", roomid, std::string(room_data), e.what()); } } return result; } QString Cache::getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event); if (res) { try { StateEvent msg = nlohmann::json::parse(std::string_view(event.data(), event.size())) .get>(); if (!msg.content.url.empty()) return QString::fromStdString(msg.content.url); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } } // We don't use an avatar for group chats. if (membersdb.size(txn) > 2) return QString(); auto cursor = lmdb::cursor::open(txn, membersdb); std::string_view user_id; std::string_view member_data; std::string fallback_url; // Resolve avatar for 1-1 chats. while (cursor.get(user_id, member_data, MDB_NEXT)) { try { MemberInfo m = nlohmann::json::parse(member_data).get(); if (user_id == localUserId_.toStdString()) { fallback_url = m.avatar_url; continue; } cursor.close(); return QString::fromStdString(m.avatar_url); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse member info: {}", e.what()); } } cursor.close(); // Default case when there is only one member. return QString::fromStdString(fallback_url); } QString Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event); if (res) { try { StateEvent msg = nlohmann::json::parse(std::string_view(event.data(), event.size())) .get>(); if (!msg.content.name.empty()) return QString::fromStdString(msg.content.name); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); } } res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event); if (res) { try { StateEvent msg = nlohmann::json::parse(std::string_view(event.data(), event.size())) .get>(); if (!msg.content.alias.empty()) return QString::fromStdString(msg.content.alias); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what()); } } auto cursor = lmdb::cursor::open(txn, membersdb); const auto total = membersdb.size(txn); std::size_t ii = 0; std::string_view user_id; std::string_view member_data; std::map members; while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { try { members.emplace(user_id, nlohmann::json::parse(member_data).get()); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse member info: {}", e.what()); } ii++; } cursor.close(); if (total == 1 && !members.empty()) return QString::fromStdString(members.begin()->second.name); auto first_member = [&members, this]() { for (const auto &m : members) { if (m.first != localUserId_.toStdString()) return QString::fromStdString(m.second.name); } return localUserId_; }(); auto second_member = [&members, this]() { bool first = true; for (const auto &m : members) { if (m.first != localUserId_.toStdString()) { if (first) first = false; else return QString::fromStdString(m.second.name); } } return localUserId_; }(); if (total == 2) return first_member; else if (total == 3) return tr("%1 and %2", "RoomName").arg(first_member, second_member); else if (total > 3) return tr("%1 and %n other(s)", "", (int)total - 2).arg(first_member); return tr("Empty Room"); } mtx::events::state::JoinRule Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); return msg.content.join_rule; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); } } return state::JoinRule::Knock; } bool Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); return msg.content.guest_access == AccessState::CanJoin; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.guest_access event: {}", e.what()); } } return false; } QString Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); if (!msg.content.topic.empty()) return QString::fromStdString(msg.content.topic); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } return QString(); } QString Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); if (!msg.content.room_version.empty()) return QString::fromStdString(msg.content.room_version); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); } } nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); return QStringLiteral("1"); } bool Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); return msg.content.type == mtx::events::state::room_type::space; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); } } nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); return false; } bool Cache::getRoomIsTombstoned(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); if (res) { try { StateEvent msg = nlohmann::json::parse(event).get>(); return true; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.tombstone event: {}", e.what()); } } return false; } QString Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event); if (res) { try { StrippedEvent msg = nlohmann::json::parse(event).get>(); return QString::fromStdString(msg.content.name); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); } } auto cursor = lmdb::cursor::open(txn, membersdb); std::string_view user_id, member_data; while (cursor.get(user_id, member_data, MDB_NEXT)) { if (user_id == localUserId_.toStdString()) continue; try { MemberInfo tmp = nlohmann::json::parse(member_data).get(); cursor.close(); return QString::fromStdString(tmp.name); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse member info: {}", e.what()); } } cursor.close(); return tr("Empty Room"); } QString Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event); if (res) { try { StrippedEvent msg = nlohmann::json::parse(event).get>(); return QString::fromStdString(msg.content.url); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } } auto cursor = lmdb::cursor::open(txn, membersdb); std::string_view user_id, member_data; while (cursor.get(user_id, member_data, MDB_NEXT)) { if (user_id == localUserId_.toStdString()) continue; try { MemberInfo tmp = nlohmann::json::parse(member_data).get(); cursor.close(); return QString::fromStdString(tmp.avatar_url); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse member info: {}", e.what()); } } cursor.close(); return QString(); } QString Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event); if (res) { try { StrippedEvent msg = nlohmann::json::parse(event).get>(); return QString::fromStdString(msg.content.topic); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } return QString(); } bool Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db) { using namespace mtx::events; using namespace mtx::events::state; std::string_view event; bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event); if (res) { try { StrippedEvent msg = nlohmann::json::parse(event).get>(); return msg.content.type == mtx::events::state::room_type::space; } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } } return false; } std::vector Cache::joinedRooms() { auto txn = ro_txn(env_); auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); std::string_view id, data; std::vector room_ids; // Gather the room ids for the joined rooms. while (roomsCursor.get(id, data, MDB_NEXT)) room_ids.emplace_back(id); roomsCursor.close(); return room_ids; } std::map Cache::getCommonRooms(const std::string &user_id) { std::map result; auto txn = ro_txn(env_); std::string_view room_id; std::string_view room_data; std::string_view member_info; auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { try { if (getMembersDb(txn, std::string(room_id)).get(txn, user_id, member_info)) { RoomInfo tmp = nlohmann::json::parse(std::move(room_data)).get(); result.emplace(std::string(room_id), std::move(tmp)); } } catch (std::exception &e) { nhlog::db()->warn("Failed to read common room for member ({}) in room ({}): {}", user_id, room_id, e.what()); } } roomsCursor.close(); return result; } std::optional Cache::getMember(const std::string &room_id, const std::string &user_id) { if (user_id.empty() || !env_.handle()) return std::nullopt; try { auto txn = ro_txn(env_); auto membersdb = getMembersDb(txn, room_id); std::string_view info; if (membersdb.get(txn, user_id, info)) { MemberInfo m = nlohmann::json::parse(info).get(); return m; } } catch (std::exception &e) { nhlog::db()->warn( "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what()); } return std::nullopt; } std::vector Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) { try { auto txn = ro_txn(env_); auto db = getMembersDb(txn, room_id); auto cursor = lmdb::cursor::open(txn, db); std::size_t currentIndex = 0; const auto endIndex = std::min(startIndex + len, db.size(txn)); std::vector members; std::string_view user_id, user_data; while (cursor.get(user_id, user_data, MDB_NEXT)) { if (currentIndex < startIndex) { currentIndex += 1; continue; } if (currentIndex >= endIndex) break; try { MemberInfo tmp = nlohmann::json::parse(user_data).get(); members.emplace_back(RoomMember{ QString::fromStdString(std::string(user_id)), QString::fromStdString(tmp.name), QString::fromStdString(tmp.avatar_url), }); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("{}", e.what()); } currentIndex += 1; } cursor.close(); return members; } catch (const lmdb::error &e) { nhlog::db()->error("Failed to retrieve members from db in room {}: {}", room_id, e.what()); return {}; } } std::optional Cache::getInviteMember(const std::string &room_id, const std::string &user_id) { if (user_id.empty() || !env_.handle()) return std::nullopt; try { auto txn = ro_txn(env_); auto membersdb = getInviteMembersDb(txn, room_id); std::string_view info; if (membersdb.get(txn, user_id, info)) { MemberInfo m = nlohmann::json::parse(info).get(); return m; } } catch (std::exception &e) { nhlog::db()->warn( "Failed to read member ({}) in invite room ({}): {}", user_id, room_id, e.what()); } return std::nullopt; } std::vector Cache::getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len) { try { auto txn = ro_txn(env_); std::vector members; auto db = getInviteMembersDb(txn, room_id); auto cursor = lmdb::cursor::open(txn, db); std::size_t currentIndex = 0; const auto endIndex = std::min(startIndex + len, db.size(txn)); std::string_view user_id, user_data; while (cursor.get(user_id, user_data, MDB_NEXT)) { if (currentIndex < startIndex) { currentIndex += 1; continue; } if (currentIndex >= endIndex) break; try { MemberInfo tmp = nlohmann::json::parse(user_data).get(); members.emplace_back(RoomMember{ QString::fromStdString(std::string(user_id)), QString::fromStdString(tmp.name), QString::fromStdString(tmp.avatar_url), tmp.is_direct, }); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("{}", e.what()); } currentIndex += 1; } cursor.close(); return members; } catch (const lmdb::error &e) { nhlog::db()->error("Failed to retrieve members from db in room {}: {}", room_id, e.what()); return {}; } } bool Cache::isRoomMember(const std::string &user_id, const std::string &room_id) { try { auto txn = ro_txn(env_); auto db = getMembersDb(txn, room_id); std::string_view value; bool res = db.get(txn, user_id, value); return res; } catch (std::exception &e) { nhlog::db()->warn( "Failed to read member membership ({}) in room ({}): {}", user_id, room_id, e.what()); } return false; } void Cache::savePendingMessage(const std::string &room_id, const mtx::events::collections::TimelineEvents &message) { auto txn = lmdb::txn::begin(env_); auto eventsDb = getEventsDb(txn, room_id); mtx::responses::Timeline timeline; timeline.events.push_back(message); saveTimelineMessages(txn, eventsDb, room_id, timeline); auto pending = getPendingMessagesDb(txn, room_id); int64_t now = QDateTime::currentMSecsSinceEpoch(); pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message)); txn.commit(); } std::vector Cache::pendingEvents(const std::string &room_id) { auto txn = ro_txn(env_); auto pending = getPendingMessagesDb(txn, room_id); std::vector related_ids; try { { auto pendingCursor = lmdb::cursor::open(txn, pending); std::string_view tsIgnored, pendingTxn; while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { related_ids.emplace_back(pendingTxn.data(), pendingTxn.size()); } } } catch (const lmdb::error &e) { nhlog::db()->error("pending events error: {}", e.what()); } return related_ids; } std::optional Cache::firstPendingMessage(const std::string &room_id) { auto txn = lmdb::txn::begin(env_); auto pending = getPendingMessagesDb(txn, room_id); try { auto pendingCursor = lmdb::cursor::open(txn, pending); std::string_view tsIgnored, pendingTxn; while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { auto eventsDb = getEventsDb(txn, room_id); std::string_view event; if (!eventsDb.get(txn, pendingTxn, event)) { pending.del(txn, tsIgnored, pendingTxn); continue; } try { mtx::events::collections::TimelineEvents te = nlohmann::json::parse(event).get(); pendingCursor.close(); return te; } catch (std::exception &e) { nhlog::db()->error("Failed to parse message from cache {}", e.what()); pending.del(txn, tsIgnored, pendingTxn); continue; } } } catch (const lmdb::error &e) { } 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); std::string_view 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, lmdb::dbi &eventsDb, const std::string &room_id, const mtx::responses::Timeline &res) { if (res.events.empty()) return; 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; std::string_view indexVal, val; uint64_t index = std::numeric_limits::max() / 2; auto cursor = lmdb::cursor::open(txn, orderDb); if (cursor.get(indexVal, val, MDB_LAST)) { index = lmdb::from_sv(indexVal); } uint64_t msgIndex = std::numeric_limits::max() / 2; auto msgCursor = lmdb::cursor::open(txn, order2msgDb); if (msgCursor.get(indexVal, val, MDB_LAST)) { msgIndex = lmdb::from_sv(indexVal); } bool first = true; for (const auto &e : res.events) { 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; } std::string_view event_id = event_id_val; nlohmann::json orderEntry = nlohmann::json::object(); orderEntry["event_id"] = event_id_val; if (first && !res.prev_batch.empty()) orderEntry["prev_batch"] = res.prev_batch; std::string_view txn_order; if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) { eventsDb.put(txn, event_id, event.dump()); eventsDb.del(txn, txn_id); std::string_view msg_txn_order; if (msg2orderDb.get(txn, txn_id, msg_txn_order)) { order2msgDb.put(txn, msg_txn_order, event_id); msg2orderDb.put(txn, event_id, msg_txn_order); msg2orderDb.del(txn, txn_id); } orderDb.put(txn, txn_order, orderEntry.dump()); evToOrderDb.put(txn, event_id, txn_order); evToOrderDb.del(txn, txn_id); auto relations = mtx::accessors::relations(e); if (!relations.relations.empty()) { for (const auto &r : relations.relations) { if (!r.event_id.empty()) { relationsDb.del(txn, r.event_id, txn_id); relationsDb.put(txn, r.event_id, event_id); } } } auto pendingCursor = lmdb::cursor::open(txn, pending); std::string_view 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>(&e)) { if (redaction->redacts.empty()) continue; // persist the first redaction in case this is a limited timeline and it is the first // event to not break pagination. if (first && res.limited) { first = false; ++index; nhlog::db()->debug("saving redaction '{}'", orderEntry.dump()); cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND); evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); eventsDb.put(txn, event_id, event.dump()); } std::string_view oldEvent; bool success = eventsDb.get(txn, redaction->redacts, oldEvent); if (!success) continue; try { auto te = nlohmann::json::parse(std::string_view(oldEvent.data(), oldEvent.size())) .get(); // overwrite the content and add redation data std::visit( [&redaction, &room_id, &txn, &eventsDb, this](auto &ev) { ev.unsigned_data.redacted_because = *redaction; ev.unsigned_data.redacted_by = redaction->event_id; if constexpr (isStateEvent_) { auto statesdb = getStatesDb(txn, room_id); auto stateskeydb = getStatesKeyDb(txn, room_id); auto membersdb = getMembersDb(txn, room_id); mtx::events::StateEvent redactedEvent; redactedEvent.event_id = ev.event_id; redactedEvent.state_key = ev.state_key; redactedEvent.type = ev.type; nhlog::db()->critical("Redacting: {}", nlohmann::json(redactedEvent).dump(2)); saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, mtx::events::collections::StateEvents{redactedEvent}); } }, te); event = mtx::accessors::serialize_event(te); event["content"].clear(); } catch (std::exception &e) { nhlog::db()->error("Failed to parse message from cache {}", e.what()); continue; } eventsDb.put(txn, redaction->redacts, event.dump()); eventsDb.put(txn, redaction->event_id, nlohmann::json(*redaction).dump()); } else { // This check protects against duplicates in the timeline. If the event_id // is already in the DB, we skip putting it (again) in ordered DBs, and only // update the event itself and its relations. std::string_view unused_read; if (!evToOrderDb.get(txn, event_id, unused_read)) { first = false; ++index; nhlog::db()->debug("saving '{}'", orderEntry.dump()); cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND); evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); // TODO(Nico): Allow blacklisting more event types in UI if (!isHiddenEvent(txn, e, room_id)) { ++msgIndex; msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND); msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex)); } } else { nhlog::db()->warn("duplicate event '{}'", orderEntry.dump()); } eventsDb.put(txn, event_id, event.dump()); auto relations = mtx::accessors::relations(e); if (!relations.relations.empty()) { for (const auto &r : relations.relations) { if (!r.event_id.empty()) { relationsDb.put(txn, r.event_id, 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); std::string_view indexVal, val; uint64_t index = std::numeric_limits::max() / 2; { auto cursor = lmdb::cursor::open(txn, orderDb); if (cursor.get(indexVal, val, MDB_FIRST)) { index = lmdb::from_sv(indexVal); } } uint64_t msgIndex = std::numeric_limits::max() / 2; { auto msgCursor = lmdb::cursor::open(txn, order2msgDb); if (msgCursor.get(indexVal, val, MDB_FIRST)) { msgIndex = lmdb::from_sv(indexVal); } } if (res.chunk.empty()) { if (orderDb.get(txn, lmdb::to_sv(index), val)) { auto orderEntry = nlohmann::json::parse(val); orderEntry["prev_batch"] = res.end; orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); txn.commit(); } return index; } std::string event_id_val; for (const auto &e : res.chunk) { if (std::holds_alternative>(e)) continue; auto event = mtx::accessors::serialize_event(e); event_id_val = event["event_id"].get(); std::string_view event_id = event_id_val; // This check protects against duplicates in the timeline. If the event_id is // already in the DB, we skip putting it (again) in ordered DBs, and only update the // event itself and its relations. std::string_view unused_read; if (!evToOrderDb.get(txn, event_id, unused_read)) { --index; nlohmann::json orderEntry = nlohmann::json::object(); orderEntry["event_id"] = event_id_val; orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); // TODO(Nico): Allow blacklisting more event types in UI if (!isHiddenEvent(txn, e, room_id)) { --msgIndex; order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id); msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex)); } } eventsDb.put(txn, event_id, event.dump()); auto relations = mtx::accessors::relations(e); if (!relations.relations.empty()) { for (const auto &r : relations.relations) { if (!r.event_id.empty()) { relationsDb.put(txn, r.event_id, event_id); } } } } if (!event_id_val.empty()) { nlohmann::json orderEntry = nlohmann::json::object(); orderEntry["event_id"] = event_id_val; orderEntry["prev_batch"] = res.end; orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); } else if (!res.chunk.empty()) { // to not break pagination, even if all events are redactions we try to persist something in // the batch. nlohmann::json orderEntry = nlohmann::json::object(); event_id_val = mtx::accessors::event_id(res.chunk.back()); --index; auto event = mtx::accessors::serialize_event(res.chunk.back()).dump(); eventsDb.put(txn, event_id_val, event); evToOrderDb.put(txn, event_id_val, lmdb::to_sv(index)); orderEntry["event_id"] = event_id_val; orderEntry["prev_batch"] = res.end; orderDb.put(txn, lmdb::to_sv(index), 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); std::string_view 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; nlohmann::json obj; try { obj = nlohmann::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) { std::string event_id = obj["event_id"].get(); if (!event_id.empty()) { evToOrderDb.del(txn, event_id); eventsDb.del(txn, event_id); relationsDb.del(txn, event_id); std::string_view order{}; bool exists = msg2orderDb.get(txn, event_id, order); if (exists) { order2msgDb.del(txn, order); msg2orderDb.del(txn, 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; std::string_view eventId; bool innerStart = true; bool found = false; while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) { innerStart = false; nlohmann::json obj; try { obj = nlohmann::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; } if (!start) { do { lmdb::cursor_del(msgCursor); } while (msgCursor.get(indexVal, val, MDB_PREV)); } cursor.close(); msgCursor.close(); txn.commit(); } void Cache::markSentNotification(const std::string &event_id) { auto txn = lmdb::txn::begin(env_); notificationsDb_.put(txn, event_id, ""); txn.commit(); } void Cache::removeReadNotification(const std::string &event_id) { auto txn = lmdb::txn::begin(env_); notificationsDb_.del(txn, event_id); txn.commit(); } bool Cache::isNotificationSent(const std::string &event_id) { auto txn = ro_txn(env_); std::string_view value; bool res = notificationsDb_.get(txn, event_id, value); return res; } std::vector Cache::getRoomIds(lmdb::txn &txn) { auto cursor = lmdb::cursor::open(txn, roomsDb_); std::vector rooms; std::string_view room_id, _unused; while (cursor.get(room_id, _unused, MDB_NEXT)) rooms.emplace_back(room_id); cursor.close(); return rooms; } void Cache::deleteOldMessages() { std::string_view indexVal, val; auto txn = lmdb::txn::begin(env_); auto room_ids = getRoomIds(txn); 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); uint64_t first, last; if (cursor.get(indexVal, val, MDB_LAST)) { last = lmdb::from_sv(indexVal); } else { continue; } if (cursor.get(indexVal, val, MDB_FIRST)) { first = lmdb::from_sv(indexVal); } else { continue; } size_t message_count = static_cast(last - first); if (message_count < MAX_RESTORED_MESSAGES) continue; bool start = true; while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) && message_count-- > MAX_RESTORED_MESSAGES) { start = false; auto obj = nlohmann::json::parse(std::string_view(val.data(), val.size())); if (obj.count("event_id") != 0) { std::string event_id = obj["event_id"].get(); evToOrderDb.del(txn, event_id); eventsDb.del(txn, event_id); relationsDb.del(txn, event_id); std::string_view order{}; bool exists = m2o.get(txn, event_id, order); if (exists) { o2m.del(txn, order); m2o.del(txn, event_id); } } cursor.del(); } cursor.close(); } txn.commit(); } void Cache::deleteOldData() noexcept { try { deleteOldMessages(); } catch (const lmdb::error &e) { nhlog::db()->error("failed to delete old messages: {}", e.what()); } } void Cache::updateSpaces(lmdb::txn &txn, const std::set &spaces_with_updates, std::set rooms_with_updates) { if (spaces_with_updates.empty() && rooms_with_updates.empty()) return; for (const auto &space : spaces_with_updates) { // delete old entries { auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); bool first = true; std::string_view sp = space, space_child = ""; if (cursor.get(sp, space_child, MDB_SET)) { while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { first = false; spacesParentsDb_.del(txn, space_child, space); } } cursor.close(); spacesChildrenDb_.del(txn, space); } for (const auto &event : getStateEventsWithType(txn, space)) { if (event.content.via.has_value() && event.state_key.size() > 3 && event.state_key.at(0) == '!') { spacesChildrenDb_.put(txn, space, event.state_key); spacesParentsDb_.put(txn, event.state_key, space); } } for (const auto &r : getRoomIds(txn)) { if (auto parent = getStateEvent(txn, r, space)) { rooms_with_updates.insert(r); } } } const auto space_event_type = to_string(mtx::events::EventType::SpaceChild); for (const auto &room : rooms_with_updates) { for (const auto &event : getStateEventsWithType(txn, room)) { if (event.content.via.has_value() && event.state_key.size() > 3 && event.state_key.at(0) == '!') { const std::string &space = event.state_key; auto pls = getStateEvent(txn, space); if (!pls) continue; if (pls->content.user_level(event.sender) >= pls->content.state_level(space_event_type)) { spacesChildrenDb_.put(txn, space, room); spacesParentsDb_.put(txn, room, space); } else { nhlog::db()->debug("Skipping {} in {} because of missing PL. {}: {} < {}", room, space, event.sender, pls->content.user_level(event.sender), pls->content.state_level(space_event_type)); } } } } } QMap> Cache::spaces() { auto txn = ro_txn(env_); QMap> ret; { auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); bool first = true; std::string_view space_id, space_child; while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) { first = false; if (!space_child.empty()) { std::string_view room_data; if (roomsDb_.get(txn, space_id, room_data)) { RoomInfo tmp = nlohmann::json::parse(std::move(room_data)).get(); ret.insert(QString::fromUtf8(space_id.data(), (int)space_id.size()), tmp); } else { ret.insert(QString::fromUtf8(space_id.data(), (int)space_id.size()), std::nullopt); } } } cursor.close(); } return ret; } std::vector Cache::getParentRoomIds(const std::string &room_id) { auto txn = ro_txn(env_); std::vector roomids; { auto cursor = lmdb::cursor::open(txn, spacesParentsDb_); bool first = true; std::string_view sp = room_id, space_parent; if (cursor.get(sp, space_parent, MDB_SET)) { while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { first = false; if (!space_parent.empty()) roomids.emplace_back(space_parent); } } cursor.close(); } return roomids; } std::vector Cache::getChildRoomIds(const std::string &room_id) { auto txn = ro_txn(env_); std::vector roomids; { auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); bool first = true; std::string_view sp = room_id, space_child; if (cursor.get(sp, space_child, MDB_SET)) { while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { first = false; if (!space_child.empty()) roomids.emplace_back(space_child); } } cursor.close(); } return roomids; } std::vector Cache::getImagePacks(const std::string &room_id, std::optional stickers) { auto txn = ro_txn(env_); std::vector infos; auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack, const std::string &source_room, const std::string &state_key, bool from_space) { bool pack_is_sticker = pack.pack ? pack.pack->is_sticker() : true; bool pack_is_emoji = pack.pack ? pack.pack->is_emoji() : true; bool pack_matches = !stickers.has_value() || (stickers.value() ? pack_is_sticker : pack_is_emoji); ImagePackInfo info; info.source_room = source_room; info.state_key = state_key; info.pack.pack = pack.pack; info.from_space = from_space; for (const auto &img : pack.images) { if (stickers.has_value() && (img.second.overrides_usage() ? (stickers.value() ? !img.second.is_sticker() : !img.second.is_emoji()) : !pack_matches)) continue; info.pack.images.insert(img); } if (!info.pack.images.empty()) infos.push_back(std::move(info)); }; // packs from account data if (auto accountpack = getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) { auto tmp = std::get_if>(&*accountpack); if (tmp) addPack(tmp->content, "", "", false); } // packs from rooms, that were enabled globally if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) { auto tmp = std::get_if>( &*roomPacks); if (tmp) { for (const auto &[room_id2, state_to_d] : tmp->content.rooms) { // don't add stickers from this room twice if (room_id2 == room_id) continue; for (const auto &[state_id, d] : state_to_d) { (void)d; if (auto pack = getStateEvent(txn, room_id2, state_id)) addPack(pack->content, room_id2, state_id, false); } } } } std::function addRoomAndCanonicalParents; std::unordered_set visitedRooms; addRoomAndCanonicalParents = [this, &addRoomAndCanonicalParents, &addPack, &visitedRooms, &txn, &room_id]( const std::string ¤t_room) { if (visitedRooms.count(current_room)) return; else visitedRooms.insert(current_room); if (auto pack = getStateEvent(txn, current_room)) { addPack(pack->content, current_room, "", current_room != room_id); } for (const auto &pack : getStateEventsWithType(txn, current_room)) { addPack(pack.content, current_room, pack.state_key, current_room != room_id); } for (const auto &parent : getStateEventsWithType(txn, current_room)) { if (parent.content.canonical && parent.content.via && !parent.content.via->empty()) { try { addRoomAndCanonicalParents(parent.state_key); } catch (const lmdb::error &) { nhlog::db()->debug("Skipping events from parent community, because we are " "not joined to it: {}", parent.state_key); } } } }; // packs from current room and then iterate canonical space parents addRoomAndCanonicalParents(room_id); return infos; } std::optional Cache::getAccountData(mtx::events::EventType type, const std::string &room_id) { auto txn = ro_txn(env_); return getAccountData(txn, type, room_id); } std::optional Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { try { auto db = getAccountDataDb(txn, room_id); std::string_view data; if (db.get(txn, to_string(type), data)) { mtx::responses::utils::RoomAccountDataEvents events; nlohmann::json j = nlohmann::json::array({ nlohmann::json::parse(data), }); mtx::responses::utils::parse_room_account_data_events(j, events); if (events.size() == 1) return events.front(); } } catch (...) { } return std::nullopt; } bool Cache::hasEnoughPowerLevel(const std::vector &eventTypes, const std::string &room_id, const std::string &user_id) { using namespace mtx::events; using namespace mtx::events::state; auto txn = ro_txn(env_); try { auto db = getStatesDb(txn, room_id); int64_t min_event_level = std::numeric_limits::max(); int64_t user_level = std::numeric_limits::min(); std::string_view event; bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event); if (res) { try { StateEvent msg = nlohmann::json::parse(std::string_view(event.data(), event.size())) .get>(); user_level = msg.content.user_level(user_id); for (const auto &ty : eventTypes) min_event_level = std::min(min_event_level, msg.content.state_level(to_string(ty))); } catch (const nlohmann::json::exception &e) { nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); } } return user_level >= min_event_level; } catch (...) { return false; } } std::vector Cache::roomMembers(const std::string &room_id) { auto txn = ro_txn(env_); try { std::vector members; std::string_view user_id, unused; auto db = getMembersDb(txn, room_id); auto cursor = lmdb::cursor::open(txn, db); while (cursor.get(user_id, unused, MDB_NEXT)) members.emplace_back(user_id); cursor.close(); return members; } catch (const lmdb::error &e) { nhlog::db()->error("Failed to retrieve members from db in room {}: {}", room_id, e.what()); return {}; } } crypto::Trust Cache::roomVerificationStatus(const std::string &room_id) { crypto::Trust trust = crypto::Verified; try { auto txn = lmdb::txn::begin(env_); auto db = getMembersDb(txn, room_id); auto keysDb = getUserKeysDb(txn); std::vector keysToRequest; std::string_view user_id, unused; auto cursor = lmdb::cursor::open(txn, db); while (cursor.get(user_id, unused, MDB_NEXT)) { auto verif = verificationStatus_(std::string(user_id), txn); if (verif.unverified_device_count) { trust = crypto::Unverified; if (verif.verified_devices.empty() && verif.no_keys) { // we probably don't have the keys yet, so query them keysToRequest.push_back(std::string(user_id)); } } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified) trust = crypto::TOFU; } if (!keysToRequest.empty()) { std::string_view token; bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); if (!result) token = ""; markUserKeysOutOfDate(txn, keysDb, keysToRequest, std::string(token)); } } catch (std::exception &e) { nhlog::db()->error("Failed to calculate verification status for {}: {}", room_id, e.what()); trust = crypto::Unverified; } return trust; } std::map> Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) { std::string_view keys; try { auto txn = ro_txn(env_); std::map> members; auto db = getMembersDb(txn, room_id); auto keysDb = getUserKeysDb(txn); std::string_view user_id, unused; auto cursor = lmdb::cursor::open(txn, db); while (cursor.get(user_id, unused, MDB_NEXT)) { auto res = keysDb.get(txn, user_id, keys); if (res) { auto k = nlohmann::json::parse(keys).get(); if (verified_only) { auto verif = verificationStatus_(std::string(user_id), txn); if (verif.user_verified == crypto::Trust::Verified || !verif.verified_devices.empty()) { auto keyCopy = k; keyCopy.device_keys.clear(); std::copy_if( k.device_keys.begin(), k.device_keys.end(), std::inserter(keyCopy.device_keys, keyCopy.device_keys.end()), [&verif](const auto &key) { auto curve25519 = key.second.keys.find("curve25519:" + key.first); if (curve25519 == key.second.keys.end()) return false; if (auto t = verif.verified_device_keys.find(curve25519->second); t == verif.verified_device_keys.end() || t->second != crypto::Trust::Verified) return false; return key.first == key.second.device_id && std::find(verif.verified_devices.begin(), verif.verified_devices.end(), key.first) != verif.verified_devices.end(); }); if (!keyCopy.device_keys.empty()) members[std::string(user_id)] = std::move(keyCopy); } } else { members[std::string(user_id)] = std::move(k); } } else { if (!verified_only) members[std::string(user_id)] = {}; } } cursor.close(); return members; } catch (std::exception &e) { nhlog::db()->debug("Error retrieving members: {}", e.what()); return {}; } } QString Cache::displayName(const QString &room_id, const QString &user_id) { return QString::fromStdString(displayName(room_id.toStdString(), user_id.toStdString())); } static bool isDisplaynameSafe(const std::string &s) { const auto str = QString::fromStdString(s); for (QChar c : str) { if (c.isPrint() && !c.isSpace()) return false; } return true; } std::string Cache::displayName(const std::string &room_id, const std::string &user_id) { if (auto info = getMember(room_id, user_id); info && !isDisplaynameSafe(info->name)) return info->name; return user_id; } QString Cache::avatarUrl(const QString &room_id, const QString &user_id) { if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); info && !info->avatar_url.empty()) return QString::fromStdString(info->avatar_url); return QString(); } mtx::events::presence::Presence Cache::presence(const std::string &user_id) { mtx::events::presence::Presence presence_{}; presence_.presence = mtx::presence::PresenceState::offline; if (user_id.empty()) return presence_; std::string_view presenceVal; auto txn = ro_txn(env_); auto res = presenceDb_.get(txn, user_id, presenceVal); if (res) { presence_ = nlohmann::json::parse(std::string_view(presenceVal.data(), presenceVal.size())) .get(); } return presence_; } void to_json(nlohmann::json &j, const UserKeyCache &info) { j["device_keys"] = info.device_keys; j["seen_device_keys"] = info.seen_device_keys; j["seen_device_ids"] = info.seen_device_ids; j["master_keys"] = info.master_keys; j["master_key_changed"] = info.master_key_changed; j["user_signing_keys"] = info.user_signing_keys; j["self_signing_keys"] = info.self_signing_keys; j["updated_at"] = info.updated_at; j["last_changed"] = info.last_changed; } void from_json(const nlohmann::json &j, UserKeyCache &info) { info.device_keys = j.value("device_keys", std::map{}); info.seen_device_keys = j.value("seen_device_keys", std::set{}); info.seen_device_ids = j.value("seen_device_ids", std::set{}); info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{}); info.master_key_changed = j.value("master_key_changed", false); info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{}); info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{}); info.updated_at = j.value("updated_at", ""); info.last_changed = j.value("last_changed", ""); } std::optional Cache::userKeys(const std::string &user_id) { auto txn = ro_txn(env_); return userKeys_(user_id, txn); } std::optional Cache::userKeys_(const std::string &user_id, lmdb::txn &txn) { std::string_view keys; try { auto db = getUserKeysDb(txn); auto res = db.get(txn, user_id, keys); if (res) { return nlohmann::json::parse(keys).get(); } else { return std::nullopt; } } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what()); return std::nullopt; } } void Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) { auto txn = lmdb::txn::begin(env_); auto db = getUserKeysDb(txn); std::map updates; for (const auto &[user, keys] : keyQuery.device_keys) updates[user].device_keys = keys; for (const auto &[user, keys] : keyQuery.master_keys) updates[user].master_keys = keys; for (const auto &[user, keys] : keyQuery.user_signing_keys) updates[user].user_signing_keys = keys; for (const auto &[user, keys] : keyQuery.self_signing_keys) updates[user].self_signing_keys = keys; for (auto &[user, update] : updates) { nhlog::db()->debug("Updated user keys: {}", user); auto updateToWrite = update; std::string_view oldKeys; auto res = db.get(txn, user, oldKeys); if (res) { updateToWrite = nlohmann::json::parse(oldKeys).get(); auto last_changed = updateToWrite.last_changed; // skip if we are tracking this and expect it to be up to date with the last // sync token if (!last_changed.empty() && last_changed != sync_token) { nhlog::db()->debug("Not storing update for user {}, because " "last_changed {}, but we fetched update for {}", user, last_changed, sync_token); continue; } if (!updateToWrite.master_keys.keys.empty() && update.master_keys.keys != updateToWrite.master_keys.keys) { nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}", user, updateToWrite.master_keys.keys.size(), update.master_keys.keys.size()); updateToWrite.master_key_changed = true; } updateToWrite.master_keys = update.master_keys; updateToWrite.self_signing_keys = update.self_signing_keys; updateToWrite.user_signing_keys = update.user_signing_keys; auto oldDeviceKeys = std::move(updateToWrite.device_keys); updateToWrite.device_keys.clear(); // Don't insert keys, which we have seen once already for (const auto &[device_id, device_keys] : update.device_keys) { if (oldDeviceKeys.count(device_id) && oldDeviceKeys.at(device_id).keys == device_keys.keys) { // this is safe, since the keys are the same updateToWrite.device_keys[device_id] = device_keys; } else { bool keyReused = false; for (const auto &[key_id, key] : device_keys.keys) { (void)key_id; if (updateToWrite.seen_device_keys.count(key)) { nhlog::crypto()->warn( "Key '{}' reused by ({}: {})", key, user, device_id); keyReused = true; break; } if (updateToWrite.seen_device_ids.count(device_id)) { nhlog::crypto()->warn("device_id '{}' reused by ({})", device_id, user); keyReused = true; break; } } if (!keyReused && !oldDeviceKeys.count(device_id)) { // ensure the key has a valid signature from itself std::string device_signing_key = "ed25519:" + device_keys.device_id; if (device_id != device_keys.device_id) { nhlog::crypto()->warn("device {}:{} has a different device id " "in the body: {}", user, device_id, device_keys.device_id); continue; } if (!device_keys.signatures.count(user) || !device_keys.signatures.at(user).count(device_signing_key)) { nhlog::crypto()->warn("device {}:{} has no signature", user, device_id); continue; } if (!device_keys.keys.count(device_signing_key) || !device_keys.keys.count("curve25519:" + device_id)) { nhlog::crypto()->warn( "Device key has no curve25519 or ed25519 key {}:{}", user, device_id); continue; } if (!mtx::crypto::ed25519_verify_signature( device_keys.keys.at(device_signing_key), nlohmann::json(device_keys), device_keys.signatures.at(user).at(device_signing_key))) { nhlog::crypto()->warn( "device {}:{} has an invalid signature", user, device_id); continue; } updateToWrite.device_keys[device_id] = device_keys; } } for (const auto &[key_id, key] : device_keys.keys) { (void)key_id; updateToWrite.seen_device_keys.insert(key); } updateToWrite.seen_device_ids.insert(device_id); } } updateToWrite.updated_at = sync_token; db.put(txn, user, nlohmann::json(updateToWrite).dump()); } txn.commit(); std::map tmp; const auto local_user = utils::localUser().toStdString(); { std::unique_lock lock(verification_storage.verification_storage_mtx); for (auto &[user_id, update] : updates) { (void)update; if (user_id == local_user) { std::swap(tmp, verification_storage.status); } else { verification_storage.status.erase(user_id); } } } for (auto &[user_id, update] : updates) { (void)update; if (user_id == local_user) { for (const auto &[user, status] : tmp) { (void)status; emit verificationStatusChanged(user); } } else { emit verificationStatusChanged(user_id); } } } void Cache::markUserKeysOutOfDate(const std::vector &user_ids) { auto currentBatchToken = nextBatchToken(); auto txn = lmdb::txn::begin(env_); auto db = getUserKeysDb(txn); markUserKeysOutOfDate(txn, db, user_ids, currentBatchToken); txn.commit(); } void Cache::markUserKeysOutOfDate(lmdb::txn &txn, lmdb::dbi &db, const std::vector &user_ids, const std::string &sync_token) { mtx::requests::QueryKeys query; query.token = sync_token; for (const auto &user : user_ids) { if (user.size() > 255) { nhlog::db()->debug("Skipping device key query for user with invalid mxid: {}", user); continue; } nhlog::db()->debug("Marking user keys out of date: {}", user); std::string_view oldKeys; UserKeyCache cacheEntry{}; auto res = db.get(txn, user, oldKeys); if (res) { try { cacheEntry = nlohmann::json::parse(std::string_view(oldKeys.data(), oldKeys.size())) .get(); } catch (std::exception &e) { nhlog::db()->error("Failed to parse {}: {}", oldKeys, e.what()); } } cacheEntry.last_changed = sync_token; db.put(txn, user, nlohmann::json(cacheEntry).dump()); query.device_keys[user] = {}; } if (!query.device_keys.empty()) http::client()->query_keys( query, [this, sync_token](const mtx::responses::QueryKeys &keys, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, static_cast(err->status_code)); return; } emit userKeysUpdate(sync_token, keys); }); } void Cache::query_keys(const std::string &user_id, std::function cb) { if (user_id.size() > 255) { nhlog::db()->debug("Skipping device key query for user with invalid mxid: {}", user_id); mtx::http::ClientError err{}; err.parse_error = "invalid mxid, more than 255 bytes"; cb({}, err); return; } mtx::requests::QueryKeys req; std::string last_changed; { auto txn = ro_txn(env_); auto cache_ = userKeys_(user_id, txn); if (cache_.has_value()) { if (cache_->updated_at == cache_->last_changed) { cb(cache_.value(), {}); return; } else nhlog::db()->info("Keys outdated for {}: {} vs {}", user_id, cache_->updated_at, cache_->last_changed); } else nhlog::db()->info("No keys found for {}", user_id); req.device_keys[user_id] = {}; if (cache_) last_changed = cache_->last_changed; req.token = last_changed; } // use context object so that we can disconnect again QObject *context{new QObject(this)}; QObject::connect( this, &Cache::userKeysUpdateFinalize, context, [cb, user_id, context_ = context, this](std::string updated_user) mutable { if (user_id == updated_user) { context_->deleteLater(); auto txn = ro_txn(env_); auto keys = this->userKeys_(user_id, txn); cb(keys.value_or(UserKeyCache{}), {}); } }, Qt::QueuedConnection); http::client()->query_keys( req, [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {},{}", mtx::errors::to_string(err->matrix_error.errcode), static_cast(err->status_code)); cb({}, err); return; } emit userKeysUpdate(last_changed, res); emit userKeysUpdateFinalize(user_id); }); } void to_json(nlohmann::json &j, const VerificationCache &info) { j["device_verified"] = info.device_verified; j["device_blocked"] = info.device_blocked; } void from_json(const nlohmann::json &j, VerificationCache &info) { info.device_verified = j.at("device_verified").get>(); info.device_blocked = j.at("device_blocked").get>(); } void to_json(nlohmann::json &j, const OnlineBackupVersion &info) { j["v"] = info.version; j["a"] = info.algorithm; } void from_json(const nlohmann::json &j, OnlineBackupVersion &info) { info.version = j.at("v").get(); info.algorithm = j.at("a").get(); } std::optional Cache::verificationCache(const std::string &user_id, lmdb::txn &txn) { std::string_view verifiedVal; auto db = getVerificationDb(txn); try { VerificationCache verified_state; auto res = db.get(txn, user_id, verifiedVal); if (res) { verified_state = nlohmann::json::parse(verifiedVal).get(); return verified_state; } else { return {}; } } catch (std::exception &) { return {}; } } void Cache::markDeviceVerified(const std::string &user_id, const std::string &key) { { std::string_view val; auto txn = lmdb::txn::begin(env_); auto db = getVerificationDb(txn); try { VerificationCache verified_state; auto res = db.get(txn, user_id, val); if (res) { verified_state = nlohmann::json::parse(val).get(); } for (const auto &device : verified_state.device_verified) if (device == key) return; verified_state.device_verified.insert(key); db.put(txn, user_id, nlohmann::json(verified_state).dump()); txn.commit(); } catch (std::exception &) { } } const auto local_user = utils::localUser().toStdString(); std::map tmp; { std::unique_lock lock(verification_storage.verification_storage_mtx); if (user_id == local_user) { std::swap(tmp, verification_storage.status); verification_storage.status.clear(); } else { verification_storage.status.erase(user_id); } } if (user_id == local_user) { for (const auto &[user, status] : tmp) { (void)status; emit verificationStatusChanged(user); } } else { emit verificationStatusChanged(user_id); } } void Cache::markDeviceUnverified(const std::string &user_id, const std::string &key) { std::string_view val; auto txn = lmdb::txn::begin(env_); auto db = getVerificationDb(txn); try { VerificationCache verified_state; auto res = db.get(txn, user_id, val); if (res) { verified_state = nlohmann::json::parse(val).get(); } verified_state.device_verified.erase(key); db.put(txn, user_id, nlohmann::json(verified_state).dump()); txn.commit(); } catch (std::exception &) { } const auto local_user = utils::localUser().toStdString(); std::map tmp; { std::unique_lock lock(verification_storage.verification_storage_mtx); if (user_id == local_user) { std::swap(tmp, verification_storage.status); } else { verification_storage.status.erase(user_id); } } if (user_id == local_user) { for (const auto &[user, status] : tmp) { (void)status; emit verificationStatusChanged(user); } } emit verificationStatusChanged(user_id); } VerificationStatus Cache::verificationStatus(const std::string &user_id) { auto txn = ro_txn(env_); return verificationStatus_(user_id, txn); } VerificationStatus Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn) { std::unique_lock lock(verification_storage.verification_storage_mtx); if (verification_storage.status.count(user_id)) return verification_storage.status.at(user_id); VerificationStatus status; // assume there is at least one unverified device until we have checked we have the device // list for that user. status.unverified_device_count = 1; status.no_keys = true; if (auto verifCache = verificationCache(user_id, txn)) { status.verified_devices = verifCache->device_verified; } const auto local_user = utils::localUser().toStdString(); crypto::Trust trustlevel = crypto::Trust::Unverified; if (user_id == local_user) { status.verified_devices.insert(http::client()->device_id()); trustlevel = crypto::Trust::Verified; } auto verifyAtLeastOneSig = [](const auto &toVerif, const std::map &keys, const std::string &keyOwner) { if (!toVerif.signatures.count(keyOwner)) return false; for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) { if (!keys.count(key_id)) continue; if (mtx::crypto::ed25519_verify_signature( keys.at(key_id), nlohmann::json(toVerif), signature)) return true; } return false; }; auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) { int currentVerifiedDevices = 0; for (const auto &device_id : status.verified_devices) { if (theirDeviceKeys.count(device_id)) currentVerifiedDevices++; } status.unverified_device_count = static_cast(theirDeviceKeys.size()) - currentVerifiedDevices; }; try { // for local user verify this device_key -> our master_key -> our self_signing_key // -> our device_keys // // for other user verify this device_key -> our master_key -> our user_signing_key // -> their master_key -> their self_signing_key -> their device_keys // // This means verifying the other user adds 2 extra steps,verifying our user_signing // key and their master key auto ourKeys = userKeys_(local_user, txn); auto theirKeys = userKeys_(user_id, txn); if (theirKeys) status.no_keys = false; if (!ourKeys || !theirKeys) { verification_storage.status[user_id] = status; return status; } // Update verified devices count to count without cross-signing updateUnverifiedDevices(theirKeys->device_keys); { auto &mk = ourKeys->master_keys; std::string dev_id = "ed25519:" + http::client()->device_id(); if (!mk.signatures.count(local_user) || !mk.signatures.at(local_user).count(dev_id) || !mtx::crypto::ed25519_verify_signature(olm::client()->identity_keys().ed25519, nlohmann::json(mk), mk.signatures.at(local_user).at(dev_id))) { nhlog::crypto()->debug("We have not verified our own master key"); verification_storage.status[user_id] = status; return status; } } auto master_keys = ourKeys->master_keys.keys; if (user_id != local_user) { bool theirMasterKeyVerified = verifyAtLeastOneSig(ourKeys->user_signing_keys, master_keys, local_user) && verifyAtLeastOneSig( theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user); if (theirMasterKeyVerified) trustlevel = crypto::Trust::Verified; else if (!theirKeys->master_key_changed) trustlevel = crypto::Trust::TOFU; else { verification_storage.status[user_id] = status; return status; } master_keys = theirKeys->master_keys.keys; } status.user_verified = trustlevel; verification_storage.status[user_id] = status; if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id)) return status; for (const auto &[device, device_key] : theirKeys->device_keys) { (void)device; try { auto identkey = device_key.keys.at("curve25519:" + device_key.device_id); if (verifyAtLeastOneSig(device_key, theirKeys->self_signing_keys.keys, user_id)) { status.verified_devices.insert(device_key.device_id); status.verified_device_keys[identkey] = trustlevel; } } catch (...) { } } updateUnverifiedDevices(theirKeys->device_keys); verification_storage.status[user_id] = status; return status; } catch (std::exception &e) { nhlog::db()->error("Failed to calculate verification status of {}: {}", user_id, e.what()); return status; } } void to_json(nlohmann::json &j, const RoomInfo &info) { j["name"] = info.name; j["topic"] = info.topic; j["avatar_url"] = info.avatar_url; j["version"] = info.version; j["is_invite"] = info.is_invite; j["is_space"] = info.is_space; j["tombst"] = info.is_tombstoned; j["join_rule"] = info.join_rule; j["guest_access"] = info.guest_access; j["app_l_ts"] = info.approximate_last_modification_ts; j["notification_count"] = info.notification_count; j["highlight_count"] = info.highlight_count; if (info.member_count != 0) j["member_count"] = info.member_count; if (info.tags.size() != 0) j["tags"] = info.tags; } void from_json(const nlohmann::json &j, RoomInfo &info) { info.name = j.at("name").get(); info.topic = j.at("topic").get(); info.avatar_url = j.at("avatar_url").get(); info.version = j.value( "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); info.is_invite = j.at("is_invite").get(); info.is_space = j.value("is_space", false); info.is_tombstoned = j.value("tombst", false); info.join_rule = j.at("join_rule").get(); info.guest_access = j.at("guest_access").get(); info.approximate_last_modification_ts = j.value("app_l_ts", 0); // workaround for bad values being stored in the past if (info.approximate_last_modification_ts < 100000000000) info.approximate_last_modification_ts = 0; info.notification_count = j.value("notification_count", 0); info.highlight_count = j.value("highlight_count", 0); if (j.count("member_count")) info.member_count = j.at("member_count").get(); if (j.count("tags")) info.tags = j.at("tags").get>(); } void to_json(nlohmann::json &j, const ReadReceiptKey &key) { j = nlohmann::json{{"event_id", key.event_id}, {"room_id", key.room_id}}; } void from_json(const nlohmann::json &j, ReadReceiptKey &key) { key.event_id = j.at("event_id").get(); key.room_id = j.at("room_id").get(); } void to_json(nlohmann::json &j, const MemberInfo &info) { j["name"] = info.name; j["avatar_url"] = info.avatar_url; if (!info.inviter.empty()) j["inviter"] = info.inviter; if (info.is_direct) j["is_direct"] = info.is_direct; if (!info.reason.empty()) j["reason"] = info.reason; } void from_json(const nlohmann::json &j, MemberInfo &info) { info.name = j.value("name", ""); info.avatar_url = j.value("avatar_url", ""); info.is_direct = j.value("is_direct", false); info.reason = j.value("reason", ""); info.inviter = j.value("inviter", ""); } void to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg) { obj["deviceids"] = msg.deviceids; } void from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg) { msg.deviceids = obj.at("deviceids").get(); } void to_json(nlohmann::json &obj, const SharedWithUsers &msg) { obj["keys"] = msg.keys; } void from_json(const nlohmann::json &obj, SharedWithUsers &msg) { msg.keys = obj.at("keys").get>(); } void to_json(nlohmann::json &obj, const GroupSessionData &msg) { obj["message_index"] = msg.message_index; obj["ts"] = msg.timestamp; obj["trust"] = msg.trusted; obj["sender_key"] = msg.sender_key; obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key; obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain; obj["currently"] = msg.currently; obj["indices"] = msg.indices; } void from_json(const nlohmann::json &obj, GroupSessionData &msg) { msg.message_index = obj.at("message_index").get(); msg.timestamp = obj.value("ts", 0ULL); msg.trusted = obj.value("trust", true); msg.sender_key = obj.value("sender_key", ""); msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", ""); msg.forwarding_curve25519_key_chain = obj.value("forwarding_curve25519_key_chain", std::vector{}); msg.currently = obj.value("currently", SharedWithUsers{}); msg.indices = obj.value("indices", std::map()); } void to_json(nlohmann::json &obj, const DevicePublicKeys &msg) { obj["ed25519"] = msg.ed25519; obj["curve25519"] = msg.curve25519; } void from_json(const nlohmann::json &obj, DevicePublicKeys &msg) { msg.ed25519 = obj.at("ed25519").get(); msg.curve25519 = obj.at("curve25519").get(); } void to_json(nlohmann::json &obj, const MegolmSessionIndex &msg) { obj["room_id"] = msg.room_id; obj["session_id"] = msg.session_id; } void from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) { msg.room_id = obj.at("room_id").get(); msg.session_id = obj.at("session_id").get(); } void to_json(nlohmann::json &obj, const StoredOlmSession &msg) { obj["ts"] = msg.last_message_ts; obj["s"] = msg.pickled_session; } void from_json(const nlohmann::json &obj, StoredOlmSession &msg) { msg.last_message_ts = obj.at("ts").get(); msg.pickled_session = obj.at("s").get(); } namespace cache { void setNeedsCompactFlag() { needsCompact = true; } void init(const QString &user_id) { instance_ = std::make_unique(user_id); } Cache * client() { return instance_.get(); } std::string displayName(const std::string &room_id, const std::string &user_id) { return instance_->displayName(room_id, user_id); } QString displayName(const QString &room_id, const QString &user_id) { return instance_->displayName(room_id, user_id); } QString avatarUrl(const QString &room_id, const QString &user_id) { return instance_->avatarUrl(room_id, user_id); } mtx::events::presence::Presence presence(const std::string &user_id) { if (!instance_) return {}; return instance_->presence(user_id); } // user cache stores user keys std::optional userKeys(const std::string &user_id) { return instance_->userKeys(user_id); } void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) { instance_->updateUserKeys(sync_token, keyQuery); } // device & user verification cache std::optional verificationStatus(const std::string &user_id) { return instance_->verificationStatus(user_id); } void markDeviceVerified(const std::string &user_id, const std::string &device) { instance_->markDeviceVerified(user_id, device); } void markDeviceUnverified(const std::string &user_id, const std::string &device) { instance_->markDeviceUnverified(user_id, device); } std::vector joinedRooms() { return instance_->joinedRooms(); } QMap roomInfo(bool withInvites) { return instance_->roomInfo(withInvites); } QHash invites() { return instance_->invites(); } QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { return instance_->getRoomName(txn, statesdb, membersdb); } mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) { return instance_->getRoomJoinRule(txn, statesdb); } bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) { return instance_->getRoomGuestAccess(txn, statesdb); } QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) { return instance_->getRoomTopic(txn, statesdb); } QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { return instance_->getRoomAvatarUrl(txn, statesdb, membersdb); } std::vector getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) { return instance_->getMembers(room_id, startIndex, len); } std::vector getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len) { return instance_->getMembersFromInvite(room_id, startIndex, len); } void saveState(const mtx::responses::Sync &res) { instance_->saveState(res); } bool isInitialized() { return instance_->isInitialized(); } std::string nextBatchToken() { return instance_->nextBatchToken(); } void deleteData() { instance_->deleteData(); } void removeInvite(lmdb::txn &txn, const std::string &room_id) { instance_->removeInvite(txn, room_id); } void removeInvite(const std::string &room_id) { instance_->removeInvite(room_id); } void removeRoom(lmdb::txn &txn, const std::string &roomid) { instance_->removeRoom(txn, roomid); } void removeRoom(const std::string &roomid) { instance_->removeRoom(roomid); } void removeRoom(const QString &roomid) { instance_->removeRoom(roomid.toStdString()); } void setup() { instance_->setup(); } bool runMigrations() { return instance_->runMigrations(); } cache::CacheVersion formatVersion() { return instance_->formatVersion(); } void setCurrentFormat() { instance_->setCurrentFormat(); } std::vector roomIds() { return instance_->roomIds(); } //! Retrieve all the user ids from a room. std::vector roomMembers(const std::string &room_id) { return instance_->roomMembers(room_id); } //! Check if the given user has power level greater than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector &eventTypes, const std::string &room_id, const std::string &user_id) { return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); } void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) { instance_->updateReadReceipt(txn, room_id, receipts); } UserReceipts readReceipts(const QString &event_id, const QString &room_id) { return instance_->readReceipts(event_id, room_id); } std::optional getEventIndex(const std::string &room_id, std::string_view event_id) { return instance_->getEventIndex(room_id, event_id); } std::optional> lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) { return instance_->lastInvisibleEventAfter(room_id, event_id); } std::optional> lastVisibleEvent(const std::string &room_id, std::string_view event_id) { return instance_->lastVisibleEvent(room_id, event_id); } RoomInfo singleRoomInfo(const std::string &room_id) { return instance_->singleRoomInfo(room_id); } std::vector roomsWithStateUpdates(const mtx::responses::Sync &res) { return instance_->roomsWithStateUpdates(res); } std::map getRoomInfo(const std::vector &rooms) { return instance_->getRoomInfo(rooms); } //! Calculates which the read status of a room. //! Whether all the events in the timeline have been read. std::string getFullyReadEventId(const std::string &room_id) { return instance_->getFullyReadEventId(room_id); } bool calculateRoomReadStatus(const std::string &room_id) { return instance_->calculateRoomReadStatus(room_id); } void calculateRoomReadStatus() { instance_->calculateRoomReadStatus(); } void markSentNotification(const std::string &event_id) { instance_->markSentNotification(event_id); } //! Removes an event from the sent notifications. void removeReadNotification(const std::string &event_id) { instance_->removeReadNotification(event_id); } //! Check if we have sent a desktop notification for the given event id. bool isNotificationSent(const std::string &event_id) { return instance_->isNotificationSent(event_id); } //! Remove old unused data. void deleteOldMessages() { instance_->deleteOldMessages(); } void deleteOldData() noexcept { instance_->deleteOldData(); } //! Retrieve all saved room ids. std::vector getRoomIds(lmdb::txn &txn) { return instance_->getRoomIds(txn); } //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { instance_->setEncryptedRoom(txn, room_id); } bool isRoomEncrypted(const std::string &room_id) { return instance_->isRoomEncrypted(room_id); } //! Check if a user is a member of the room. bool isRoomMember(const std::string &user_id, const std::string &room_id) { return instance_->isRoomMember(user_id, room_id); } // // Outbound Megolm Sessions // void saveOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { instance_->saveOutboundMegolmSession(room_id, data, session); } OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id) { return instance_->getOutboundMegolmSession(room_id); } bool outboundMegolmSessionExists(const std::string &room_id) noexcept { return instance_->outboundMegolmSessionExists(room_id); } void updateOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { instance_->updateOutboundMegolmSession(room_id, data, session); } void dropOutboundMegolmSession(const std::string &room_id) { instance_->dropOutboundMegolmSession(room_id); } void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) { instance_->importSessionKeys(keys); } mtx::crypto::ExportedSessionKeys exportSessionKeys() { return instance_->exportSessionKeys(); } // // Inbound Megolm Sessions // void saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session, const GroupSessionData &data) { instance_->saveInboundMegolmSession(index, std::move(session), data); } mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index) { return instance_->getInboundMegolmSession(index); } bool inboundMegolmSessionExists(const MegolmSessionIndex &index) { return instance_->inboundMegolmSessionExists(index); } std::optional getMegolmSessionData(const MegolmSessionIndex &index) { return instance_->getMegolmSessionData(index); } // // Olm Sessions // void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session, uint64_t timestamp) { instance_->saveOlmSession(curve25519, std::move(session), timestamp); } std::vector getOlmSessions(const std::string &curve25519) { return instance_->getOlmSessions(curve25519); } std::optional getOlmSession(const std::string &curve25519, const std::string &session_id) { return instance_->getOlmSession(curve25519, session_id); } std::optional getLatestOlmSession(const std::string &curve25519) { return instance_->getLatestOlmSession(curve25519); } void saveOlmAccount(const std::string &pickled) { instance_->saveOlmAccount(pickled); } std::string restoreOlmAccount() { return instance_->restoreOlmAccount(); } void storeSecret(std::string_view name, const std::string &secret) { instance_->storeSecret(name, secret); } std::optional secret(std::string_view name) { return instance_->secret(name); } std::vector getImagePacks(const std::string &room_id, std::optional stickers) { return instance_->getImagePacks(room_id, stickers); } } // namespace cache #define NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(Content) \ template std::optional> Cache::getStateEvent( \ lmdb::txn &txn, const std::string &room_id, std::string_view state_key); \ \ template std::vector> Cache::getStateEventsWithType( \ lmdb::txn &txn, const std::string &room_id, mtx::events::EventType type); NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Aliases) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Avatar) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::CanonicalAlias) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Create) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Encryption) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::GuestAccess) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::HistoryVisibility) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::JoinRules) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Member) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Name) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PinnedEvents) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::PowerLevels) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Tombstone) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::ServerAcl) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Topic) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::Widget) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::policy_rule::UserRule) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::policy_rule::RoomRule) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::policy_rule::ServerRule) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Child) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::state::space::Parent) NHEKO_CACHE_GET_STATE_EVENT_DEFINITION(mtx::events::msc2545::ImagePack)