|
|
|
@ -55,6 +55,10 @@ constexpr auto BATCH_SIZE = 100; |
|
|
|
|
//! Format: room_id -> RoomInfo
|
|
|
|
|
constexpr auto ROOMS_DB("rooms"); |
|
|
|
|
constexpr auto INVITES_DB("invites"); |
|
|
|
|
//! maps each room to its parent space (id->id)
|
|
|
|
|
constexpr auto SPACES_PARENTS_DB("space_parents"); |
|
|
|
|
//! maps each space to its current children (id->id)
|
|
|
|
|
constexpr auto SPACES_CHILDREN_DB("space_children"); |
|
|
|
|
//! Information that must be kept between sync requests.
|
|
|
|
|
constexpr auto SYNC_STATE_DB("sync_state"); |
|
|
|
|
//! Read receipts per room/event.
|
|
|
|
@ -240,6 +244,8 @@ Cache::setup() |
|
|
|
|
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); |
|
|
|
@ -1194,6 +1200,9 @@ Cache::saveState(const mtx::responses::Sync &res) |
|
|
|
|
|
|
|
|
|
auto userKeyCacheDb = getUserKeysDb(txn); |
|
|
|
|
|
|
|
|
|
std::set<std::string> spaces_with_updates; |
|
|
|
|
std::set<std::string> rooms_with_space_updates; |
|
|
|
|
|
|
|
|
|
// Save joined rooms
|
|
|
|
|
for (const auto &room : res.rooms.join) { |
|
|
|
|
auto statesdb = getStatesDb(txn, room.first); |
|
|
|
@ -1212,6 +1221,41 @@ Cache::saveState(const mtx::responses::Sync &res) |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
if (updatedInfo.is_space) { |
|
|
|
|
bool space_updates = false; |
|
|
|
|
for (const auto &e : room.second.state.events) |
|
|
|
|
if (std::holds_alternative<StateEvent<state::space::Child>>(e) || |
|
|
|
|
std::holds_alternative<StateEvent<state::PowerLevels>>(e)) |
|
|
|
|
space_updates = true; |
|
|
|
|
for (const auto &e : room.second.timeline.events) |
|
|
|
|
if (std::holds_alternative<StateEvent<state::space::Child>>(e) || |
|
|
|
|
std::holds_alternative<StateEvent<state::PowerLevels>>(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<StateEvent<state::space::Parent>>(&e)) { |
|
|
|
|
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<StateEvent<state::space::Parent>>(&e)) { |
|
|
|
|
spaces_with_updates.insert(se->state_key); |
|
|
|
|
room_has_space_update = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (room_has_space_update) |
|
|
|
|
rooms_with_space_updates.insert(room.first); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
bool has_new_tags = false; |
|
|
|
|
// Process the account_data associated with this room
|
|
|
|
@ -1291,6 +1335,8 @@ Cache::saveState(const mtx::responses::Sync &res) |
|
|
|
|
|
|
|
|
|
removeLeftRooms(txn, res.rooms.leave); |
|
|
|
|
|
|
|
|
|
updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates)); |
|
|
|
|
|
|
|
|
|
txn.commit(); |
|
|
|
|
|
|
|
|
|
std::map<QString, bool> readStatus; |
|
|
|
@ -1339,6 +1385,7 @@ Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::I |
|
|
|
|
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, json(updatedInfo).dump()); |
|
|
|
@ -1427,27 +1474,6 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) |
|
|
|
|
return rooms; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<std::string> |
|
|
|
|
Cache::roomsWithTagUpdates(const mtx::responses::Sync &res) |
|
|
|
|
{ |
|
|
|
|
using namespace mtx::events; |
|
|
|
|
|
|
|
|
|
std::vector<std::string> rooms; |
|
|
|
|
for (const auto &room : res.rooms.join) { |
|
|
|
|
bool hasUpdates = false; |
|
|
|
|
for (const auto &evt : room.second.account_data.events) { |
|
|
|
|
if (std::holds_alternative<AccountDataEvent<account_data::Tags>>(evt)) { |
|
|
|
|
hasUpdates = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (hasUpdates) |
|
|
|
|
rooms.emplace_back(room.first); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return rooms; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
RoomInfo |
|
|
|
|
Cache::singleRoomInfo(const std::string &room_id) |
|
|
|
|
{ |
|
|
|
@ -2337,6 +2363,29 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) |
|
|
|
|
return QString("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<Create> msg = json::parse(event); |
|
|
|
|
|
|
|
|
|
return msg.content.type == mtx::events::state::room_type::space; |
|
|
|
|
} catch (const 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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::optional<mtx::events::state::CanonicalAlias> |
|
|
|
|
Cache::getRoomAliases(const std::string &roomid) |
|
|
|
|
{ |
|
|
|
@ -2464,6 +2513,27 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) |
|
|
|
|
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<Create> msg = json::parse(event); |
|
|
|
|
return msg.content.type == mtx::events::state::room_type::space; |
|
|
|
|
} catch (const json::exception &e) { |
|
|
|
|
nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<std::string> |
|
|
|
|
Cache::joinedRooms() |
|
|
|
|
{ |
|
|
|
@ -2506,42 +2576,6 @@ Cache::getMember(const std::string &room_id, const std::string &user_id) |
|
|
|
|
return std::nullopt; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<RoomSearchResult> |
|
|
|
|
Cache::searchRooms(const std::string &query, std::uint8_t max_items) |
|
|
|
|
{ |
|
|
|
|
std::multimap<int, std::pair<std::string, RoomInfo>> items; |
|
|
|
|
|
|
|
|
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); |
|
|
|
|
auto cursor = lmdb::cursor::open(txn, roomsDb_); |
|
|
|
|
|
|
|
|
|
std::string_view room_id, room_data; |
|
|
|
|
while (cursor.get(room_id, room_data, MDB_NEXT)) { |
|
|
|
|
RoomInfo tmp = json::parse(room_data); |
|
|
|
|
|
|
|
|
|
const int score = utils::levenshtein_distance( |
|
|
|
|
query, QString::fromStdString(tmp.name).toLower().toStdString()); |
|
|
|
|
items.emplace(score, std::make_pair(room_id, tmp)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
cursor.close(); |
|
|
|
|
|
|
|
|
|
auto end = items.begin(); |
|
|
|
|
|
|
|
|
|
if (items.size() >= max_items) |
|
|
|
|
std::advance(end, max_items); |
|
|
|
|
else if (items.size() > 0) |
|
|
|
|
std::advance(end, items.size()); |
|
|
|
|
|
|
|
|
|
std::vector<RoomSearchResult> results; |
|
|
|
|
for (auto it = items.begin(); it != end; it++) { |
|
|
|
|
results.push_back(RoomSearchResult{it->second.first, it->second.second}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
txn.commit(); |
|
|
|
|
|
|
|
|
|
return results; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<RoomMember> |
|
|
|
|
Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) |
|
|
|
|
{ |
|
|
|
@ -3203,6 +3237,147 @@ Cache::deleteOldData() noexcept |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void |
|
|
|
|
Cache::updateSpaces(lmdb::txn &txn, |
|
|
|
|
const std::set<std::string> &spaces_with_updates, |
|
|
|
|
std::set<std::string> 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<mtx::events::state::space::Child>(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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels); |
|
|
|
|
|
|
|
|
|
for (const auto &room : rooms_with_updates) { |
|
|
|
|
for (const auto &event : |
|
|
|
|
getStateEventsWithType<mtx::events::state::space::Parent>(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<mtx::events::state::PowerLevels>(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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
QMap<QString, std::optional<RoomInfo>> |
|
|
|
|
Cache::spaces() |
|
|
|
|
{ |
|
|
|
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); |
|
|
|
|
|
|
|
|
|
QMap<QString, std::optional<RoomInfo>> 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 = json::parse(std::move(room_data)); |
|
|
|
|
ret.insert( |
|
|
|
|
QString::fromUtf8(space_id.data(), space_id.size()), tmp); |
|
|
|
|
} else { |
|
|
|
|
ret.insert( |
|
|
|
|
QString::fromUtf8(space_id.data(), space_id.size()), |
|
|
|
|
std::nullopt); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
cursor.close(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ret; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<std::string> |
|
|
|
|
Cache::getParentRoomIds(const std::string &room_id) |
|
|
|
|
{ |
|
|
|
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); |
|
|
|
|
|
|
|
|
|
std::vector<std::string> 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<std::string> |
|
|
|
|
Cache::getChildRoomIds(const std::string &room_id) |
|
|
|
|
{ |
|
|
|
|
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); |
|
|
|
|
|
|
|
|
|
std::vector<std::string> 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::optional<mtx::events::collections::RoomAccountDataEvents> |
|
|
|
|
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) |
|
|
|
|
{ |
|
|
|
@ -3884,6 +4059,7 @@ to_json(json &j, const RoomInfo &info) |
|
|
|
|
j["avatar_url"] = info.avatar_url; |
|
|
|
|
j["version"] = info.version; |
|
|
|
|
j["is_invite"] = info.is_invite; |
|
|
|
|
j["is_space"] = info.is_space; |
|
|
|
|
j["join_rule"] = info.join_rule; |
|
|
|
|
j["guest_access"] = info.guest_access; |
|
|
|
|
|
|
|
|
@ -3903,6 +4079,7 @@ from_json(const json &j, RoomInfo &info) |
|
|
|
|
info.version = j.value( |
|
|
|
|
"version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); |
|
|
|
|
info.is_invite = j.at("is_invite"); |
|
|
|
|
info.is_space = j.value("is_space", false); |
|
|
|
|
info.join_rule = j.at("join_rule"); |
|
|
|
|
info.guest_access = j.at("guest_access"); |
|
|
|
|
|
|
|
|
@ -4158,12 +4335,6 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) |
|
|
|
|
return instance_->getRoomAvatarUrl(txn, statesdb, membersdb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
QString |
|
|
|
|
getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) |
|
|
|
|
{ |
|
|
|
|
return instance_->getRoomVersion(txn, statesdb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<RoomMember> |
|
|
|
|
getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) |
|
|
|
|
{ |
|
|
|
@ -4305,11 +4476,7 @@ roomsWithStateUpdates(const mtx::responses::Sync &res) |
|
|
|
|
{ |
|
|
|
|
return instance_->roomsWithStateUpdates(res); |
|
|
|
|
} |
|
|
|
|
std::vector<std::string> |
|
|
|
|
roomsWithTagUpdates(const mtx::responses::Sync &res) |
|
|
|
|
{ |
|
|
|
|
return instance_->roomsWithTagUpdates(res); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::map<QString, RoomInfo> |
|
|
|
|
getRoomInfo(const std::vector<std::string> &rooms) |
|
|
|
|
{ |
|
|
|
@ -4329,12 +4496,6 @@ calculateRoomReadStatus() |
|
|
|
|
instance_->calculateRoomReadStatus(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::vector<RoomSearchResult> |
|
|
|
|
searchRooms(const std::string &query, std::uint8_t max_items) |
|
|
|
|
{ |
|
|
|
|
return instance_->searchRooms(query, max_items); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void |
|
|
|
|
markSentNotification(const std::string &event_id) |
|
|
|
|
{ |
|
|
|
|