Add persistent hierarchy cache (plugin DB, TTL 15m); v1.0.5

This commit is contained in:
Eddy de Vink 2026-09-01 18:21:32 +02:00
commit 47ed0599fa
4 changed files with 86 additions and 9 deletions

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -1,4 +1,6 @@
import asyncio
import json
import time
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import quote
@ -6,10 +8,35 @@ import aiohttp
from maubot import Plugin, MessageEvent
from maubot.handlers import command
from mautrix.util.async_db import Connection, Scheme, UpgradeTable
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
_REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=30)
upgrade_table = UpgradeTable()
@upgrade_table.register(description="Initial revision")
async def upgrade_v1(conn: Connection, scheme: Scheme) -> 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: