From 47ed0599faadb98ca91a7081fc0769b93ee06579 Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Tue, 1 Sep 2026 18:21:32 +0200 Subject: [PATCH] Add persistent hierarchy cache (plugin DB, TTL 15m); v1.0.5 --- base-config.yaml | 8 +++++ maubot.yaml | 4 ++- roomsoverview.mbp | Bin 3325 -> 4289 bytes roomsoverview.py | 81 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/base-config.yaml b/base-config.yaml index bf4c69a..7f77244 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -6,7 +6,15 @@ space_id: "!vervang-mij:example.org" # Hoe diep de bot in geneste subspaces mag kijken. max_depth: 5 +# Hoeveel rooms er per bericht getoond worden (Matrix-events hebben een max grootte, +# dus bij ~200 rooms wordt de lijst automatisch over meerdere berichten verdeeld). +rooms_per_message: 50 # Harde bovengrens voor het totale aantal rooms dat wordt opgehaald/verstuurd. # Voorkomt dat een enorme (gefedereerde) space-boom leidt tot duizenden berichten. max_rooms: 300 + +# Hoe lang (minuten) de opgehaalde lijst wordt gecached in de plugin-database. +# Binnen deze periode gebruikt !rooms de cache i.p.v. de hele boom opnieuw op te +# halen (minder traffic naar de homeserver en federatie). 0 = caching uit. +cache_ttl_minutes: 15 \ No newline at end of file diff --git a/maubot.yaml b/maubot.yaml index caa892e..73e7ec2 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,10 +1,12 @@ maubot: 0.1.0 id: nl.roomsoverview -version: 1.0.4 +version: 1.0.5 license: MIT modules: - roomsoverview main_class: RoomsOverviewPlugin +database: true +database_type: asyncpg config: true extra_files: - base-config.yaml diff --git a/roomsoverview.mbp b/roomsoverview.mbp index 75206151c851f750447db9740f389e551c244493..9e98239a904fe45960014ae54019274863c3846e 100644 GIT binary patch literal 4289 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 Q7o-3w& None: + if scheme == Scheme.SQLITE: + await conn.execute( + """CREATE TABLE roomsoverview_cache ( + space_id TEXT PRIMARY KEY, + rooms TEXT NOT NULL, + complete INTEGER NOT NULL, + fetched_at BIGINT NOT NULL + )""" + ) + else: + await conn.execute( + """CREATE TABLE roomsoverview_cache ( + space_id TEXT PRIMARY KEY, + rooms TEXT NOT NULL, + complete BOOLEAN NOT NULL, + fetched_at BIGINT NOT NULL + )""" + ) + def _escape_md(text: str) -> str: """Escape markdown-significant characters when embedding user content.""" @@ -31,6 +58,7 @@ class Config(BaseProxyConfig): helper.copy("max_depth") helper.copy("rooms_per_message") helper.copy("max_rooms") + helper.copy("cache_ttl_minutes") class RoomsOverviewPlugin(Plugin): @@ -41,6 +69,10 @@ class RoomsOverviewPlugin(Plugin): def get_config_class(cls): return Config + @classmethod + def get_db_upgrade_table(cls) -> UpgradeTable: + return upgrade_table + async def _fetch_hierarchy(self, space_id: str) -> Tuple[List[Dict[str, Any]], bool]: """Fetch the paginated space hierarchy for a room. @@ -123,6 +155,31 @@ class RoomsOverviewPlugin(Plugin): break return rooms, True + async def _load_cache( + self, space_id: str + ) -> Optional[Tuple[List[Dict[str, Any]], bool, int]]: + row = await self.database.fetchrow( + "SELECT rooms, complete, fetched_at FROM roomsoverview_cache " + "WHERE space_id = $1", + space_id, + ) + if not row: + return None + return json.loads(row["rooms"]), bool(row["complete"]), int(row["fetched_at"]) + + async def _store_cache( + self, space_id: str, rooms: List[Dict[str, Any]], complete: bool + ) -> None: + await self.database.execute( + "INSERT INTO roomsoverview_cache (space_id, rooms, complete, fetched_at) " + "VALUES ($1, $2, $3, $4) " + "ON CONFLICT (space_id) DO UPDATE SET rooms = $2, complete = $3, fetched_at = $4", + space_id, + json.dumps(rooms, ensure_ascii=False), + complete, + int(time.time()), + ) + @command.new(name="rooms", help="Toon alle rooms in de geconfigureerde space") async def handle_rooms(self, evt: MessageEvent) -> None: space_id = str(self.config.get("space_id", "") or "") @@ -133,14 +190,24 @@ class RoomsOverviewPlugin(Plugin): return await evt.mark_read() - entries, complete = await self._fetch_hierarchy(space_id) - rooms = [ - r - for r in entries - if r.get("room_id") != space_id - and (r.get("room_type") or "") != "m.space" - ] + ttl_minutes = max(0, int(self.config.get("cache_ttl_minutes", 15) or 15)) + cached = await self._load_cache(space_id) + now = int(time.time()) + if cached and ttl_minutes > 0 and now - cached[2] < ttl_minutes * 60: + rooms, complete = cached[0], cached[1] + self.log.info( + f"rooms: cache hit voor {space_id} (age {now - cached[2]}s)" + ) + else: + entries, complete = await self._fetch_hierarchy(space_id) + rooms = [ + r + for r in entries + if r.get("room_id") != space_id + and (r.get("room_type") or "") != "m.space" + ] + await self._store_cache(space_id, rooms, complete) if not rooms: if not complete: