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 7520615..9e98239 100644 Binary files a/roomsoverview.mbp and b/roomsoverview.mbp differ diff --git a/roomsoverview.py b/roomsoverview.py index cfd1c55..3dad5df 100644 --- a/roomsoverview.py +++ b/roomsoverview.py @@ -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: