From 874e84af765ac0de51ba1698053e29f97877587a Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Fri, 4 Sep 2026 15:54:30 +0200 Subject: [PATCH] Render !rooms entries as blockquote cards (name/members, topic, join link); v2.1.6 --- .gitignore | 4 +++ maubot.yaml | 2 +- roomsoverview.mbp | Bin 5113 -> 5633 bytes roomsoverview.py | 87 ++++++++++++++++++++++++++++------------------ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 8182bc4..e14b68d 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,10 @@ jspm_packages/ ehthumbs.db Thumbs.db +# Python +__pycache__/ +*.pyc + # Test files test/fixtures/ test/sample-pdfs/ diff --git a/maubot.yaml b/maubot.yaml index 3f17638..a45975e 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,6 +1,6 @@ maubot: 0.1.0 id: nl.roomsoverview -version: 2.1.2 +version: 2.1.6 license: MIT modules: - roomsoverview diff --git a/roomsoverview.mbp b/roomsoverview.mbp index fd48316e2afec3d6109f1de6aa5568776b0ed442..4eacc28a8db818fea4003ae2f3b32f658d957dc8 100644 GIT binary patch delta 5195 zcmZ|TWmweRx(4tehLrAZ1O^b1mXwl4dKhBp5Rh*86N5oZcMHPMNQbm^GXl~bk}4hN zdf$Eab@o2{ob_padG59D=i~3{aVRgKt%iX~4g!I2KrzAWrU=#Qj6@I!G!6oR@Imw- zXA3XOH=g`{7S2xkdbl7An$ZkLy3q_rFJF8R8s;A~5a{n={g_d_ZUJJ%@eTP6I>BuC zyS7m5SG@sq#SYK`twGM&j&{D3uo4Tqj(J?YRz0DH{UYO4GppY`CFD@{QU39t%hhGB zk+kCw543TVq8sg-?^K8x!(#;u%kYPKS^Ahi>;70&g_K z5&u6|aR|F}-w;W7FxLuobr&vrS1iTU8c-6mjk0=8`rV-%3d?aCa&u!QNf0Z*iNLgZ z&SQ8u?KCUZbq8E$xFd4dC_7Us??RnO^2PS&E2jubvfft)Q3<vr-1AKUyS`~uKD$#l7M0t&Cil$G;Z-9=U=Fzv4d#(2; z?h5`_f$bYyi1}zh_Ck`ZVKq#tJ@uXD&eBOQAamOQ91&eJU1RK?w)SW~)-AX~w+Xk? zf8|^~m9cS0%>u4wUrq1rP6;qKs%a-d&#Pe4@kkmnd_D(^>NI;py29qfpV2p!S# zsD6rUG1e!X2QgIs#m?vWGOfer73n&7 zhb5$snPRT3J%8IyJjUG_PO5^|63)lSo|Mi1Qln)XrNS^4;| z3+#xzhuFWsTnV7Q$(FDQLU|q30+Fg9(@R`sL6h!n_ zF^b$b01`+O?ODqMJkRSOw#xX@W2CNCvUnQZV(`X8k=yk~yB^i?(NEnwtrdF2#mM)| zS5LuDKepLq+mCFxzF^B$V$o>S#_hm+3|mNX61V*zvj*jL7q!=!WDenATyeH2$jGFe zD2le{3Dmx@Mnhh639CCUd1|^(0Rf)k)E{@^nt`0nNfX6`&A>~uCkuP9sfXdczTJY+ z>GIr_@COGFeB-2-hz|Ku90*t<;xh<4W7W#SHcFZy?k_cVF3_3(gt_5;EIpbzmw^lKE%1lpD z06b3X#oT=7BR@F&gSgTdy8rFLr?g5?rIcJ2LKQb&-f9!z%fyq%`*M81`$=}6h zUkwcR5^qhRS+$BWZ-1|Q(j_)Pv7V+EEZy~9i}Jmp{@NIxQL@d$HNXDL{NiaXxi zl#1PpKn^l88SOc4%3g;&8%^_-9p?AD-=EEw_bur7(&UD;XEd!aqE9PSQ&rjJ0H8mW z$xr+74Lw3E6fIhJwLPOx3AOr$k>n;0a5?(*~Nml^Dp`K3E=T(}^+OFqc zVhmYUpv;7)=citHwqa)UBs()0X-53Y>he`F7VM{ez3V%ZRhZorgTif9NT`Z1{ViZ=153u@#`zVY4 z^Q?@~SIPM1Lpa8CvrIfIqmyiZ0Fnk-q?E+y|Lfc~|6IfW4?zmRS IS;fQbGOXaq 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 delta 4691 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 DB5}Vt diff --git a/roomsoverview.py b/roomsoverview.py index 9e69e09..5290a3d 100644 --- a/roomsoverview.py +++ b/roomsoverview.py @@ -244,8 +244,37 @@ class RoomsOverviewPlugin(Plugin): pl = int(users.get(user_id, default) or 0) return pl >= MOD_POWER_LEVEL + async def _room_exists(self, room_id: str) -> bool: + """Bestaat de room nog? Alleen een duidelijke 404 telt als "weg". + + 200 en 403 (geen lid) betekenen "bestaat". Netwerk- en andere fouten + worden als "bestaat" behandeld, zodat er nooit per ongeluk data + verdwijnt. + """ + base = self._base_url() + h = self._headers() + rid = quote(room_id, safe="") + for path in ( + f"{base}/_matrix/client/v3/rooms/{rid}/state/m.room.create", + f"{base}/_matrix/client/v3/rooms/{rid}/joined_members", + ): + try: + async with self.http.get( + path, headers=h, timeout=_REQUEST_TIMEOUT + ) as r: + if r.status == 404: + continue + return True # 200, 403, … alles behalve 404 = bestaat + except Exception: + return True + return False + async def _validate_all(self) -> Tuple[int, int]: - """Valideer alle rooms in de index; verwijder verdwenen. (removed, total)""" + """Valideer: verwijder alleen rooms die niet meer bestaan (404). + + Werkt metadata NOOIT bij, alleen een duidelijke 404 op meerdere + endpoints verwijdert een rij. (removed, total) + """ async with self._refresh_lock: rows = await self.database.fetch( "SELECT room_id FROM roomsoverview_rooms" @@ -253,24 +282,11 @@ class RoomsOverviewPlugin(Plugin): removed = 0 for row in rows: rid = row["room_id"] - info = await self._get_room_info(rid) - if info is None: + if not await self._room_exists(rid): await self.database.execute( "DELETE FROM roomsoverview_rooms WHERE room_id = $1", rid ) removed += 1 - else: - await self.database.execute( - "UPDATE roomsoverview_rooms SET display_name = $2, " - "canonical_alias = $3, topic = $4, member_count = $5, " - "last_checked = $6 WHERE room_id = $1", - rid, - info["name"] or None, - info["alias"] or None, - info["topic"] or None, - info["members"], - int(time.time()), - ) if removed: self.log.info(f"validatie: {removed} room(s) verwijderd (bestaan niet meer)") return removed, len(rows) @@ -301,35 +317,40 @@ class RoomsOverviewPlugin(Plugin): rooms.sort(key=sort_key) - lines = [] + rooms_blocks = [] for r in rooms: display = r.get("display_name") or "" alias = r.get("canonical_alias") or "" room_id = r["room_id"] - link_target = quote(alias if alias else room_id, safe="!") member_count = r.get("member_count") topic = (r.get("topic") or "").strip().replace("\n", " ") - suffix = f" ({member_count} leden)" if member_count is not None else "" - if display: - label = _escape_md(display) - else: - label = room_id - link = f"[{_escape_md(alias) if alias else room_id}](https://matrix.to/#/{link_target})" - if display: - line = f"* **{label}**{suffix} — {link}" - else: - line = f"* {link}{suffix}" + # Elke room wordt een blockquote-kaart: een afgebakend blok met + # gekleurde kantlijn, naam + ledental als kop, omschrijving eronder + # en onderaan de join-link. + link_target = alias if alias else room_id + address = _escape_md(alias if alias else room_id) + lines = [] + title = _escape_md(display) if display else address + meta = f"**{title}**" + if member_count is not None: + meta += f" · {member_count} leden" + lines.append(meta) if topic: - line += f" — {_escape_md(topic)}" - lines.append(line) + lines.append(_escape_md(topic)) + # Linktekst = volledige matrix.to URL (fragment mag rauw # en : bevatten) + lines.append(f"[{address}](https://matrix.to/#/{link_target})") + rooms_blocks.append("\n".join(f"> {l}" for l in lines)) chunk_size = max(1, int(self.config.get("rooms_per_message", 50) or 50)) - chunks = [lines[i : i + chunk_size] for i in range(0, len(lines), chunk_size)] - total = len(lines) + chunks = [ + rooms_blocks[i : i + chunk_size] + for i in range(0, len(rooms_blocks), chunk_size) + ] + total = len(rooms_blocks) for idx, chunk in enumerate(chunks, start=1): header = f"**Rooms overzicht ({total} rooms getoond, deel {idx}/{len(chunks)}):**\n\n" - await evt.respond(header + "\n".join(chunk), markdown=True) + await evt.respond(header + "\n\n".join(chunk), markdown=True) @command.new( name="rooms", @@ -380,7 +401,7 @@ class RoomsOverviewPlugin(Plugin): target = info["alias"] or room_id await evt.reply( f"Room toegevoegd: **{info['name'] or room_id}** " - f"([{target}](https://matrix.to/#/{quote(target, safe='!')}))" + f"([{target}](https://matrix.to/#/{target}))" ) @handle_rooms.subcommand(name="remove", help="Verwijder een room (moderator+)")