Skip to content
Snippets Groups Projects
Cache.cc 31.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU General Public License as published by
     * the Free Software Foundation, either version 3 of the License, or
     * (at your option) any later version.
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU General Public License for more details.
     *
     * You should have received a copy of the GNU General Public License
     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     */
    
    #include <stdexcept>
    
    
    #include <QByteArray>
    
    #include <QDebug>
    #include <QFile>
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    #include <QHash>
    
    #include <QStandardPaths>
    
    
    #include <variant.hpp>
    
    
    #include "Cache.h"
    
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! Should be changed when a breaking change occurs in the cache format.
    //! This will reset client's data.
    
    static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.04.21");
    
    static const lmdb::val NEXT_BATCH_KEY("next_batch");
    
    static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    //! 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 const char *ROOMS_DB   = "rooms";
    static constexpr const char *INVITES_DB = "rooms";
    //! Keeps already downloaded media for reuse.
    //! Format: matrix_url -> binary data.
    static constexpr const char *MEDIA_DB = "media";
    //! Information that  must be kept between sync requests.
    static constexpr const char *SYNC_STATE_DB = "sync_state";
    //! Read receipts per room/event.
    static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
    
    
    using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
    using Receipts       = std::map<std::string, std::map<std::string, uint64_t>>;
    
    
    Cache::Cache(const QString &userId, QObject *parent)
      : QObject{parent}
      , env_{nullptr}
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
      , syncStateDb_{0}
      , roomsDb_{0}
    
      , invitesDb_{0}
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
      , mediaDb_{0}
    
      , readReceiptsDb_{0}
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
      , localUserId_{userId}
    
    
    void
    Cache::setup()
    {
            qDebug() << "Setting up cache";
    
    
            auto statePath = QString("%1/%2/state")
                               .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                               .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
    
            cacheDirectory_ = QString("%1/%2")
                                .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                                .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
    
            bool isInitial = !QFile::exists(statePath);
    
            env_.set_mapsize(256UL * 1024UL * 1024UL); /* 256 MB */
    
                    qDebug() << "First time initializing LMDB";
    
                    if (!QDir().mkpath(statePath)) {
                            throw std::runtime_error(
                              ("Unable to create state directory:" + statePath).toStdString().c_str());
                    }
            }
    
            try {
                    env_.open(statePath.toStdString().c_str());
            } 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()));
                    }
    
                    qWarning() << "Resetting cache due to LMDB version mismatch:" << e.what();
    
                    for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
                            if (!stateDir.remove(file))
                                    throw std::runtime_error(
                                      ("Unable to delete file " + file).toStdString().c_str());
                    }
    
                    env_.open(statePath.toStdString().c_str());
            }
    
            auto txn        = lmdb::txn::begin(env_);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            syncStateDb_    = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
            roomsDb_        = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
            invitesDb_      = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
            mediaDb_        = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
            readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            qRegisterMetaType<RoomInfo>();
    
    void
    Cache::saveImage(const QString &url, const QByteArray &image)
    {
            auto key = url.toUtf8();
    
            try {
                    auto txn = lmdb::txn::begin(env_);
    
                    lmdb::dbi_put(txn,
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                                  mediaDb_,
    
                                  lmdb::val(key.data(), key.size()),
                                  lmdb::val(image.data(), image.size()));
    
                    txn.commit();
            } catch (const lmdb::error &e) {
                    qCritical() << "saveImage:" << e.what();
            }
    }
    
    QByteArray
    Cache::image(const QString &url) const
    {
    
            if (url.isEmpty())
                    return QByteArray();
    
    
            auto key = url.toUtf8();
    
            try {
                    auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
                    lmdb::val image;
    
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                    bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(key.data(), key.size()), image);
    
    
                    txn.commit();
    
                    if (!res)
                            return QByteArray();
    
                    return QByteArray(image.data(), image.size());
            } catch (const lmdb::error &e) {
    
                    qCritical() << "image:" << e.what() << url;
    
    void
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::removeInvite(const std::string &room_id)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            auto txn = lmdb::txn::begin(env_);
            lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr);
            txn.commit();
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::removeRoom(lmdb::txn &txn, const std::string &roomid)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::removeRoom(const std::string &roomid)
    
    {
            auto txn = lmdb::txn::begin(env_, nullptr, 0);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_put(txn, syncStateDb_, NEXT_BATCH_KEY, lmdb::val(token.data(), token.size()));
    
    void
    
    Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            setNextBatchToken(txn, token.toStdString());
    
    bool
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            lmdb::val token;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            bool res = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
    
    QString
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            lmdb::val token;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
    
            return QString::fromUtf8(token.data(), token.size());
    
    
    void
    Cache::deleteData()
    {
            qInfo() << "Deleting cache data";
    
            if (!cacheDirectory_.isEmpty())
                    QDir(cacheDirectory_).removeRecursively();
    }
    
    
    bool
    Cache::isFormatValid()
    {
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
            lmdb::val current_version;
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version);
    
    
            txn.commit();
    
            if (!res)
                    return false;
    
            std::string stored_version(current_version.data(), current_version.size());
    
            if (stored_version != CURRENT_CACHE_FORMAT_VERSION) {
                    qWarning() << "Stored format version" << QString::fromStdString(stored_version);
                    qWarning() << "There are breaking changes in the cache format.";
                    return false;
            }
    
            return true;
    }
    
    void
    Cache::setCurrentFormat()
    {
            auto txn = lmdb::txn::begin(env_);
    
            lmdb::dbi_put(
              txn,
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
              syncStateDb_,
    
              CACHE_FORMAT_VERSION_KEY,
              lmdb::val(CURRENT_CACHE_FORMAT_VERSION.data(), CURRENT_CACHE_FORMAT_VERSION.size()));
    
            txn.commit();
    }
    
    Cache::readReceipts(const QString &event_id, const QString &room_id)
    {
    
    
            ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
            nlohmann::json json_key = receipt_key;
    
            try {
                    auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
                    auto key = json_key.dump();
    
                    lmdb::val value;
    
                    bool res =
                      lmdb::dbi_get(txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), value);
    
                    txn.commit();
    
                    if (res) {
                            auto json_response = json::parse(std::string(value.data(), value.size()));
    
                            auto values        = json_response.get<std::map<std::string, uint64_t>>();
    
                            for (const auto &v : values)
    
                                    // timestamp, user_id
                                    receipts.emplace(v.second, v.first);
    
                    }
    
            } catch (const lmdb::error &e) {
                    qCritical() << "readReceipts:" << e.what();
            }
    
            return receipts;
    }
    
    void
    
    Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
    
            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();
    
                            lmdb::val prev_value;
    
                            bool exists = lmdb::dbi_get(
    
                              txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), prev_value);
    
                            std::map<std::string, uint64_t> 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 =
                                      json::parse(std::string(prev_value.data(), prev_value.size()));
    
                                    // Retrieve the saved receipts.
    
                                    saved_receipts = json_value.get<std::map<std::string, uint64_t>>();
    
                            }
    
                            // Append the new ones.
    
                            for (const auto &event_receipt : event_receipts)
    
                                    saved_receipts.emplace(event_receipt.first, event_receipt.second);
    
    
                            // Save back the merged (or only the new) receipts.
                            nlohmann::json json_updated_value = saved_receipts;
                            std::string merged_receipts       = json_updated_value.dump();
    
                            lmdb::dbi_put(txn,
                                          readReceiptsDb_,
                                          lmdb::val(key.data(), key.size()),
                                          lmdb::val(merged_receipts.data(), merged_receipts.size()));
    
                    } catch (const lmdb::error &e) {
                            qCritical() << "updateReadReceipts:" << e.what();
                    }
            }
    }
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    
    void
    Cache::saveState(const mtx::responses::Sync &res)
    {
            auto txn = lmdb::txn::begin(env_);
    
            setNextBatchToken(txn, res.next_batch);
    
            // Save joined rooms
            for (const auto &room : res.rooms.join) {
                    auto statesdb  = getStatesDb(txn, room.first);
                    auto membersdb = getMembersDb(txn, room.first);
    
                    saveStateEvents(txn, statesdb, membersdb, room.first, room.second.state.events);
                    saveStateEvents(txn, statesdb, membersdb, room.first, room.second.timeline.events);
    
                    RoomInfo updatedInfo;
                    updatedInfo.name  = getRoomName(txn, statesdb, membersdb).toStdString();
                    updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString();
                    updatedInfo.avatar_url =
                      getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first))
                        .toStdString();
    
                    lmdb::dbi_put(
                      txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump()));
    
    
                    updateReadReceipt(txn, room.first, room.second.ephemeral.receipts);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            }
    
            saveInvites(txn, res.rooms.invite);
    
            removeLeftRooms(txn, res.rooms.leave);
    
            txn.commit();
    }
    
    void
    Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &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_invite = true;
    
                    lmdb::dbi_put(
                      txn, invitesDb_, lmdb::val(room.first), lmdb::val(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 (mpark::holds_alternative<StrippedEvent<Member>>(e)) {
                            auto msg = mpark::get<StrippedEvent<Member>>(e);
    
                            auto display_name = msg.content.display_name.empty()
                                                  ? msg.state_key
                                                  : msg.content.display_name;
    
                            MemberInfo tmp{display_name, msg.content.avatar_url};
    
                            lmdb::dbi_put(
                              txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump()));
                    } else {
                            mpark::visit(
                              [&txn, &statesdb](auto msg) {
                                      bool res = lmdb::dbi_put(txn,
                                                               statesdb,
                                                               lmdb::val(to_string(msg.type)),
                                                               lmdb::val(json(msg).dump()));
    
                                      if (!res)
                                              std::cout << "couldn't save data" << json(msg).dump()
                                                        << '\n';
                              },
                              e);
                    }
            }
    }
    
    std::vector<std::string>
    Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
    {
            std::vector<std::string> rooms;
            for (const auto &room : res.rooms.join) {
                    bool hasUpdates = false;
                    for (const auto &s : room.second.state.events) {
                            if (containsStateUpdates(s)) {
                                    hasUpdates = true;
                                    break;
                            }
                    }
    
                    for (const auto &s : room.second.timeline.events) {
                            if (containsStateUpdates(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;
    }
    
    std::map<QString, RoomInfo>
    Cache::getRoomInfo(const std::vector<std::string> &rooms)
    {
            std::map<QString, RoomInfo> room_info;
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
            for (const auto &room : rooms) {
                    lmdb::val data;
    
                    // Check if the room is joined.
                    if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room), data)) {
                            try {
                                    room_info.emplace(
                                      QString::fromStdString(room),
                                      json::parse(std::string(data.data(), data.size())));
                            } catch (const json::exception &e) {
                                    qWarning()
                                      << "failed to parse room info:" << QString::fromStdString(room)
                                      << QString::fromStdString(std::string(data.data(), data.size()));
                            }
                    } else {
                            // Check if the room is an invite.
                            if (lmdb::dbi_get(txn, invitesDb_, lmdb::val(room), data)) {
                                    try {
                                            room_info.emplace(
                                              QString::fromStdString(room),
                                              json::parse(std::string(data.data(), data.size())));
                                    } catch (const json::exception &e) {
                                            qWarning() << "failed to parse room info for invite:"
                                                       << QString::fromStdString(room)
                                                       << QString::fromStdString(
                                                            std::string(data.data(), data.size()));
                                    }
                            }
                    }
            }
    
            txn.commit();
    
            return room_info;
    }
    
    QMap<QString, RoomInfo>
    
    Cache::roomInfo(bool withInvites)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    {
            QMap<QString, RoomInfo> result;
    
    
            auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    
            std::string room_id;
            std::string room_data;
    
            // Gather info about the joined rooms.
    
            auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            while (roomsCursor.get(room_id, room_data, MDB_NEXT)) {
    
                    RoomInfo tmp = json::parse(std::move(room_data));
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
                    result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp));
            }
    
            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 = json::parse(room_data);
                            result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp));
                    }
                    invitesCursor.close();
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975
            }
    
            txn.commit();
    
            return result;
    }
    
    QString
    Cache::getRoomAvatarUrl(lmdb::txn &txn,
                            lmdb::dbi &statesdb,
                            lmdb::dbi &membersdb,
                            const QString &room_id)
    {
            using namespace mtx::events;
            using namespace mtx::events::state;
    
            lmdb::val event;
            bool res = lmdb::dbi_get(
              txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event);
    
            if (res) {
                    try {
                            StateEvent<Avatar> msg =
                              json::parse(std::string(event.data(), event.size()));
    
                            return QString::fromStdString(msg.content.url);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(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 user_id;
            std::string member_data;
    
            // Resolve avatar for 1-1 chats.
            while (cursor.get(user_id, member_data, MDB_NEXT)) {
                    if (user_id == localUserId_.toStdString())
                            continue;
    
                    try {
                            MemberInfo m = json::parse(member_data);
    
                            cursor.close();
                            return QString::fromStdString(m.avatar_url);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            cursor.close();
    
            // Default case when there is only one member.
            return avatarUrl(room_id, localUserId_);
    }
    
    QString
    Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
    {
            using namespace mtx::events;
            using namespace mtx::events::state;
    
            lmdb::val event;
            bool res = lmdb::dbi_get(
              txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event);
    
            if (res) {
                    try {
                            StateEvent<Name> msg = json::parse(std::string(event.data(), event.size()));
    
                            if (!msg.content.name.empty())
                                    return QString::fromStdString(msg.content.name);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            res = lmdb::dbi_get(
              txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event);
    
            if (res) {
                    try {
                            StateEvent<CanonicalAlias> msg =
                              json::parse(std::string(event.data(), event.size()));
    
                            if (!msg.content.alias.empty())
                                    return QString::fromStdString(msg.content.alias);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            auto cursor     = lmdb::cursor::open(txn, membersdb);
            const int total = membersdb.size(txn);
    
            std::size_t ii = 0;
            std::string user_id;
            std::string member_data;
            std::map<std::string, MemberInfo> members;
    
            while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) {
                    try {
                            members.emplace(user_id, json::parse(member_data));
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(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_;
            }();
    
            if (total == 2)
                    return first_member;
            else if (total > 2)
                    return QString("%1 and %2 others").arg(first_member).arg(total);
    
            return "Empty Room";
    }
    
    QString
    Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
    {
            using namespace mtx::events;
            using namespace mtx::events::state;
    
            lmdb::val event;
            bool res = lmdb::dbi_get(
              txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event);
    
            if (res) {
                    try {
                            StateEvent<Topic> msg =
                              json::parse(std::string(event.data(), event.size()));
    
                            if (!msg.content.topic.empty())
                                    return QString::fromStdString(msg.content.topic);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            return QString();
    }
    
    QString
    Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
    {
            using namespace mtx::events;
            using namespace mtx::events::state;
    
            lmdb::val event;
            bool res = lmdb::dbi_get(
              txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event);
    
            if (res) {
                    try {
                            StrippedEvent<state::Name> msg =
                              json::parse(std::string(event.data(), event.size()));
                            return QString::fromStdString(msg.content.name);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            auto cursor = lmdb::cursor::open(txn, membersdb);
            std::string user_id, member_data;
    
            while (cursor.get(user_id, member_data, MDB_NEXT)) {
                    if (user_id == localUserId_.toStdString())
                            continue;
    
                    try {
                            MemberInfo tmp = json::parse(member_data);
                            cursor.close();
    
                            return QString::fromStdString(tmp.name);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            cursor.close();
    
            return QString("Empty Room");
    }
    
    QString
    Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
    {
            using namespace mtx::events;
            using namespace mtx::events::state;
    
            lmdb::val event;
            bool res = lmdb::dbi_get(
              txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event);
    
            if (res) {
                    try {
                            StrippedEvent<state::Avatar> msg =
                              json::parse(std::string(event.data(), event.size()));
                            return QString::fromStdString(msg.content.url);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            auto cursor = lmdb::cursor::open(txn, membersdb);
            std::string user_id, member_data;
    
            while (cursor.get(user_id, member_data, MDB_NEXT)) {
                    if (user_id == localUserId_.toStdString())
                            continue;
    
                    try {
                            MemberInfo tmp = json::parse(member_data);
                            cursor.close();
    
                            return QString::fromStdString(tmp.avatar_url);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            cursor.close();
    
            return QString();
    }
    
    QString
    Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
    {
            using namespace mtx::events;
            using namespace mtx::events::state;
    
            lmdb::val event;
            bool res =
              lmdb::dbi_get(txn, db, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event);
    
            if (res) {
                    try {
                            StrippedEvent<Topic> msg =
                              json::parse(std::string(event.data(), event.size()));
                            return QString::fromStdString(msg.content.topic);
                    } catch (const json::exception &e) {
                            qWarning() << QString::fromStdString(e.what());
                    }
            }
    
            return QString();
    }
    
    std::vector<std::string>
    Cache::joinedRooms()
    {
            auto txn         = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
    
            std::string id, data;
            std::vector<std::string> room_ids;
    
            // Gather the room ids for the joined rooms.
            while (roomsCursor.get(id, data, MDB_NEXT))
                    room_ids.emplace_back(id);
    
            roomsCursor.close();
            txn.commit();
    
            return room_ids;
    }
    
    void
    Cache::populateMembers()
    {
            auto rooms = joinedRooms();
            qDebug() << "loading" << rooms.size() << "rooms";
    
            auto txn = lmdb::txn::begin(env_);
    
            for (const auto &room : rooms) {
                    const auto roomid = QString::fromStdString(room);
    
                    auto membersdb = getMembersDb(txn, room);
                    auto cursor    = lmdb::cursor::open(txn, membersdb);
    
                    std::string user_id, info;
                    while (cursor.get(user_id, info, MDB_NEXT)) {
                            MemberInfo m = json::parse(info);
    
                            const auto userid = QString::fromStdString(user_id);
    
                            insertDisplayName(roomid, userid, QString::fromStdString(m.name));
                            insertAvatarUrl(roomid, userid, QString::fromStdString(m.avatar_url));
                    }
    
                    cursor.close();
            }
    
            txn.commit();
    }
    
    QVector<SearchResult>
    Cache::getAutocompleteMatches(const std::string &room_id,
                                  const std::string &query,
                                  std::uint8_t max_items)
    {
            std::multimap<int, std::pair<std::string, std::string>> items;
    
            auto txn    = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
            auto cursor = lmdb::cursor::open(txn, getMembersDb(txn, room_id));
    
            std::string user_id, user_data;
            while (cursor.get(user_id, user_data, MDB_NEXT)) {
                    const auto display_name = displayName(room_id, user_id);
                    const int score         = utils::levenshtein_distance(query, display_name);
    
                    items.emplace(score, std::make_pair(user_id, display_name));
            }
    
            auto end = items.begin();
    
            if (items.size() >= max_items)
                    std::advance(end, max_items);
            else if (items.size() > 0)
                    std::advance(end, items.size());
    
            QVector<SearchResult> results;
            for (auto it = items.begin(); it != end; it++) {
                    const auto user = it->second;
                    results.push_back(SearchResult{QString::fromStdString(user.first),
                                                   QString::fromStdString(user.second)});
            }
    
            return results;
    }
    
    QHash<QString, QString> Cache::DisplayNames;
    QHash<QString, QString> Cache::AvatarUrls;
    
    QString
    Cache::displayName(const QString &room_id, const QString &user_id)
    {
            auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
            if (DisplayNames.contains(fmt))
                    return DisplayNames[fmt];
    
            return user_id;
    }
    
    std::string
    Cache::displayName(const std::string &room_id, const std::string &user_id)
    {
            auto fmt = QString::fromStdString(room_id + " " + user_id);
            if (DisplayNames.contains(fmt))
                    return DisplayNames[fmt].toStdString();
    
            return user_id;
    }
    
    QString
    Cache::avatarUrl(const QString &room_id, const QString &user_id)
    {
            auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
            if (AvatarUrls.contains(fmt))
                    return AvatarUrls[fmt];
    
            return QString();
    }
    
    void
    Cache::insertDisplayName(const QString &room_id,
                             const QString &user_id,
                             const QString &display_name)
    {
            auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
            DisplayNames.insert(fmt, display_name);
    }
    
    void
    Cache::removeDisplayName(const QString &room_id, const QString &user_id)
    {
            auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
            DisplayNames.remove(fmt);
    }
    
    void
    Cache::insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url)
    {
            auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
            AvatarUrls.insert(fmt, avatar_url);
    }
    
    void
    Cache::removeAvatarUrl(const QString &room_id, const QString &user_id)
    {
            auto fmt = QString("%1 %2").arg(room_id).arg(user_id);
            AvatarUrls.remove(fmt);
    }