Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6c9fd8004 | ||
|
|
c73b5e53c7 | ||
|
|
812f30d333 | ||
|
|
11c8c5ca15 | ||
|
|
efc68c6f06 | ||
|
|
874e84af76 |
4 changed files with 437 additions and 77 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -101,6 +101,10 @@ jspm_packages/
|
|||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Test files
|
||||
test/fixtures/
|
||||
test/sample-pdfs/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
maubot: 0.1.0
|
||||
id: nl.roomsoverview
|
||||
version: 2.1.2
|
||||
version: 2.4.0
|
||||
license: MIT
|
||||
modules:
|
||||
- roomsoverview
|
||||
|
|
|
|||
Binary file not shown.
508
roomsoverview.py
508
roomsoverview.py
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import html
|
||||
import json
|
||||
import time
|
||||
from functools import partial
|
||||
|
|
@ -9,6 +10,7 @@ import aiohttp
|
|||
|
||||
from maubot import Plugin, MessageEvent
|
||||
from maubot.handlers import command
|
||||
from mautrix.types import Format, MessageType, TextMessageEventContent
|
||||
from mautrix.util.async_db import Connection, Scheme, UpgradeTable
|
||||
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
||||
|
||||
|
|
@ -74,18 +76,42 @@ async def upgrade_v2(conn: Connection, scheme: Scheme) -> None:
|
|||
await conn.execute("DROP TABLE IF EXISTS roomsoverview_cache")
|
||||
|
||||
|
||||
def _escape_md(text: str) -> str:
|
||||
"""Escape markdown-significant characters when embedding user content."""
|
||||
return (
|
||||
text.replace("\\", "\\\\")
|
||||
.replace("`", "\\`")
|
||||
.replace("*", "\\*")
|
||||
.replace("_", "\\_")
|
||||
.replace("[", "\\[")
|
||||
.replace("]", "\\]")
|
||||
.replace("<", "\\<")
|
||||
.replace(">", "\\>")
|
||||
)
|
||||
@upgrade_table.register(description="Room avatars")
|
||||
async def upgrade_v3(conn: Connection, scheme: Scheme) -> None:
|
||||
# avatar = mxc:// URL van m.room.avatar (of NULL als room geen/ontoegankelijk
|
||||
# icoon heeft). Alleen in de lijst getoond als de URL naar een echte
|
||||
# thumbnail leidt.
|
||||
if scheme == Scheme.SQLITE:
|
||||
# SQLite kent geen ADD COLUMN IF NOT EXISTS; de kolom bestaat al zodra
|
||||
# een eerdere (gedeeltelijk gefaalde) poging hem heeft aangemaakt.
|
||||
try:
|
||||
await conn.execute(
|
||||
"ALTER TABLE roomsoverview_rooms ADD COLUMN avatar TEXT"
|
||||
)
|
||||
except Exception:
|
||||
pass # duplicate column -> al aanwezig, prima
|
||||
else:
|
||||
await conn.execute(
|
||||
"ALTER TABLE roomsoverview_rooms "
|
||||
"ADD COLUMN IF NOT EXISTS avatar TEXT"
|
||||
)
|
||||
|
||||
|
||||
def _parse_edit_args(args: str) -> Optional[Tuple[str, str, str]]:
|
||||
"""Parse `!rooms edit <room> <veld>=<waarde>`.
|
||||
|
||||
Retourneert (room_ref, field, value) of None bij ongeldige vorm.
|
||||
field is genormaliseerd naar lowercase; value mag leeg zijn (wissen).
|
||||
"""
|
||||
parts = args.strip().split(" ", 1)
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
room_ref, kv = parts
|
||||
if "=" not in kv:
|
||||
return None
|
||||
field, _, value = kv.partition("=")
|
||||
field = field.strip().lower()
|
||||
return room_ref, field, value.strip()
|
||||
|
||||
|
||||
class Config(BaseProxyConfig):
|
||||
|
|
@ -159,10 +185,11 @@ class RoomsOverviewPlugin(Plugin):
|
|||
pass
|
||||
return None
|
||||
|
||||
name, topic, alias = await asyncio.gather(
|
||||
name, topic, alias, avatar = await asyncio.gather(
|
||||
state("m.room.name"),
|
||||
state("m.room.topic"),
|
||||
state("m.room.canonical_alias"),
|
||||
state("m.room.avatar"),
|
||||
)
|
||||
|
||||
members: Optional[int] = None
|
||||
|
|
@ -184,6 +211,7 @@ class RoomsOverviewPlugin(Plugin):
|
|||
"name": ((name or {}).get("name", "") or "").strip(),
|
||||
"topic": ((topic or {}).get("topic", "") or "").strip(),
|
||||
"alias": ((alias or {}).get("alias", "") or "").strip(),
|
||||
"avatar": ((avatar or {}).get("url", "") or "").strip() or None,
|
||||
"members": members,
|
||||
}
|
||||
|
||||
|
|
@ -244,103 +272,369 @@ 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).
|
||||
|
||||
Vult daarnaast de avatar (m.room.avatar) in van rooms die er nog geen
|
||||
hebben; rooms waar de bot geen state mag lezen (403) of die simpelweg
|
||||
geen icoon hebben blijven op NULL. Metadata wordt verder nooit
|
||||
overschreven. (removed, total)
|
||||
"""
|
||||
async with self._refresh_lock:
|
||||
rows = await self.database.fetch(
|
||||
"SELECT room_id FROM roomsoverview_rooms"
|
||||
"SELECT room_id, avatar FROM roomsoverview_rooms"
|
||||
)
|
||||
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()),
|
||||
)
|
||||
continue
|
||||
if not row.get("avatar"):
|
||||
mxc = await self._get_avatar_mxc(rid)
|
||||
if mxc:
|
||||
await self.database.execute(
|
||||
"UPDATE roomsoverview_rooms SET avatar = $2 "
|
||||
"WHERE room_id = $1",
|
||||
rid,
|
||||
mxc,
|
||||
)
|
||||
if removed:
|
||||
self.log.info(f"validatie: {removed} room(s) verwijderd (bestaan niet meer)")
|
||||
return removed, len(rows)
|
||||
|
||||
async def _get_avatar_mxc(self, room_id: str) -> Optional[str]:
|
||||
"""haal de mxc-URL van m.room.avatar op (None als afwezig/ontoegankelijk)."""
|
||||
base = self._base_url()
|
||||
h = self._headers()
|
||||
rid = quote(room_id, safe="")
|
||||
try:
|
||||
async with self.http.get(
|
||||
f"{base}/_matrix/client/v3/rooms/{rid}/state/m.room.avatar",
|
||||
headers=h,
|
||||
timeout=_REQUEST_TIMEOUT,
|
||||
) as r:
|
||||
if r.status == 200:
|
||||
d = await r.json()
|
||||
url = str((d or {}).get("url", "") or "").strip()
|
||||
return url if url.startswith("mxc://") else None
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _enrich_avatars_online(self) -> int:
|
||||
"""Vul avatars in via de publieke matrixrooms.info zoek-API.
|
||||
|
||||
Alleen voor rooms met NULL avatar: query op alias-kern (of naam),
|
||||
match op room_id/alias/naam, en sla de gevonden avatar-URL op.
|
||||
Gebruikt de HTTP-URL direct (de matrixrooms-media wordt via de
|
||||
homeserver-media-proxy van de kijker opgehaald); is daarna stabiel
|
||||
genoeg voor het overzicht. Retourneert aantal gevulde rooms.
|
||||
"""
|
||||
rows = await self.database.fetch(
|
||||
"SELECT room_id, display_name, canonical_alias FROM roomsoverview_rooms "
|
||||
"WHERE alive AND (avatar IS NULL OR avatar = '')"
|
||||
)
|
||||
base = "https://api.matrixrooms.info/search"
|
||||
filled = 0
|
||||
headers = {"Accept": "application/json", "User-Agent": "maubot-roomsoverview"}
|
||||
for row in rows:
|
||||
rid = row["room_id"]
|
||||
alias = (row.get("canonical_alias") or "").strip()
|
||||
name = (row.get("display_name") or "").strip()
|
||||
q = (alias.lstrip("#") if alias else name)
|
||||
q = q.split(":")[0].strip() # zonder server-deel (API geeft 204 op 'alias:server')
|
||||
if not q:
|
||||
continue
|
||||
try:
|
||||
async with self.http.get(
|
||||
f"{base}?q={quote(q)}", headers=headers,
|
||||
timeout=_REQUEST_TIMEOUT,
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
continue
|
||||
res = await r.json()
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(res, list) or not res:
|
||||
continue
|
||||
hit = next((x for x in res if x.get("id") == rid), None)
|
||||
if not hit:
|
||||
hit = next((x for x in res if x.get("alias") == alias), None)
|
||||
if not hit:
|
||||
nl = (name or "").lower()
|
||||
hit = next((x for x in res if (x.get("name") or "").lower() == nl), None)
|
||||
if not hit:
|
||||
continue
|
||||
av = (str(hit.get("avatar_url") or hit.get("avatar") or "")).strip()
|
||||
if not av:
|
||||
continue
|
||||
await self.database.execute(
|
||||
"UPDATE roomsoverview_rooms SET avatar = $2 WHERE room_id = $1",
|
||||
rid, av,
|
||||
)
|
||||
filled += 1
|
||||
if filled:
|
||||
self.log.info(f"avatars gevuld via matrixrooms.info: {filled}")
|
||||
return filled
|
||||
|
||||
def _mxc_to_http(self, mxc: Optional[str]) -> Optional[str]:
|
||||
"""mxc://server/media_id -> HTTPS-thumbnail-URL op de eigen homeserver.
|
||||
|
||||
Element/federatie mogen media van vreemde servers niet zomaar laden;
|
||||
via de eigen (bot)homeserver geproxied werkt het overal. 64px-crop is
|
||||
klein genoeg voor een kaart-icoon.
|
||||
"""
|
||||
if not mxc or not mxc.startswith("mxc://"):
|
||||
return None
|
||||
try:
|
||||
server, media = mxc[6:].split("/", 1)
|
||||
except ValueError:
|
||||
return None
|
||||
if not server or not media:
|
||||
return None
|
||||
return (
|
||||
f"{self._base_url()}/_matrix/media/v3/thumbnail/"
|
||||
f"{quote(server, safe='')}/{quote(media, safe='')}"
|
||||
f"?width=64&height=64&method=crop"
|
||||
)
|
||||
|
||||
async def _list_rooms(self) -> List[Dict[str, Any]]:
|
||||
rows = await self.database.fetch(
|
||||
"SELECT room_id, display_name, canonical_alias, topic, member_count "
|
||||
"SELECT room_id, display_name, canonical_alias, topic, member_count, avatar "
|
||||
"FROM roomsoverview_rooms WHERE alive ORDER BY "
|
||||
"COALESCE(display_name, canonical_alias, room_id) ASC"
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def _sort_rooms(self, rooms: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
return sorted(
|
||||
rooms,
|
||||
key=lambda r: (
|
||||
r.get("display_name") or r.get("canonical_alias") or r["room_id"]
|
||||
).lower(),
|
||||
)
|
||||
|
||||
def _build_cards(self, rooms: List[Dict[str, Any]]) -> List[Tuple[str, str]]:
|
||||
"""Bouw per room een blockquote-kaart (html, plain)."""
|
||||
|
||||
# Elke room wordt een blockquote-kaart: icoontje + naam/leden als kop,
|
||||
# omschrijving eronder, onderaan de join-link. De formatted_body bouwen
|
||||
# we zelf op (veilig ge-escaped), zodat we per kaart een klein avatar-
|
||||
# icoon kunnen tonen — plain markdown kan geen width/height sturen en
|
||||
# allow_html=True op user-content is een injectierisico.
|
||||
#
|
||||
# Avatar-bron: ofwel een al-publieke HTTP-URL (bijv. via de
|
||||
# matrixrooms.info media-CDN, opgeslagen in de DB), ofwel een mxc://
|
||||
# die we via de eigen homeserver als thumbnail tonen (werkt ook voor
|
||||
# remote media, mits die server publiek serveert).
|
||||
cards = []
|
||||
for r in rooms:
|
||||
display = (r.get("display_name") or "").strip()
|
||||
alias = (r.get("canonical_alias") or "").strip()
|
||||
room_id = r["room_id"]
|
||||
member_count = r.get("member_count")
|
||||
topic = (r.get("topic") or "").strip().replace("\n", " ")
|
||||
avatar = r.get("avatar") or ""
|
||||
if avatar.startswith("https://") or avatar.startswith("http://"):
|
||||
avatar_http = avatar
|
||||
else:
|
||||
avatar_http = self._mxc_to_http(avatar) if avatar.startswith("mxc://") else None
|
||||
|
||||
link_target = alias if alias else room_id
|
||||
address = alias if alias else room_id
|
||||
name = display if display else address
|
||||
|
||||
# ---- plain text (body) ----
|
||||
plain_lines = []
|
||||
plain_title = name
|
||||
if member_count is not None:
|
||||
plain_title += f" · {member_count} leden"
|
||||
plain_lines.append(plain_title)
|
||||
if topic:
|
||||
plain_lines.append(topic)
|
||||
plain_lines.append(f"{address} (https://matrix.to/#/{link_target})")
|
||||
plain = "\n".join(plain_lines)
|
||||
|
||||
# ---- formatted HTML ----
|
||||
e = html.escape
|
||||
avatar_html = ""
|
||||
if avatar_http:
|
||||
avatar_html = (
|
||||
f'<img src="{e(avatar_http, quote=True)}" width="32" height="32" '
|
||||
f'alt="" style="vertical-align:middle;margin-right:6px">'
|
||||
)
|
||||
title_html = f"<b>{e(name)}</b>"
|
||||
if member_count is not None:
|
||||
title_html += f" · {member_count} leden"
|
||||
body_parts = [f"<blockquote><p>{avatar_html}{title_html}<br>"]
|
||||
if topic:
|
||||
body_parts.append(f"{e(topic)}<br>")
|
||||
body_parts.append(
|
||||
f'<a href="https://matrix.to/#/{e(link_target, quote=True)}">'
|
||||
f"{e(address)}</a></p></blockquote>"
|
||||
)
|
||||
cards.append(("".join(body_parts), plain))
|
||||
return cards
|
||||
|
||||
async def _send_cards(
|
||||
self,
|
||||
evt: MessageEvent,
|
||||
cards: List[Tuple[str, str]],
|
||||
subject: str,
|
||||
count_label: str,
|
||||
) -> None:
|
||||
chunk_size = max(1, int(self.config.get("rooms_per_message", 50) or 50))
|
||||
chunks = [cards[i : i + chunk_size] for i in range(0, len(cards), chunk_size)]
|
||||
total = len(cards)
|
||||
|
||||
for idx, chunk in enumerate(chunks, start=1):
|
||||
header = f"{subject} ({total} {count_label}, deel {idx}/{len(chunks)}):"
|
||||
html_body = (
|
||||
f'<p><b>{html.escape(header)}</b></p>'
|
||||
+ "".join(html_part for html_part, _ in chunk)
|
||||
)
|
||||
plain_body = header + "\n\n" + "\n\n".join(plain for _, plain in chunk)
|
||||
content = TextMessageEventContent(
|
||||
msgtype=MessageType.TEXT,
|
||||
body=plain_body,
|
||||
format=Format.HTML,
|
||||
formatted_body=html_body,
|
||||
)
|
||||
await evt.respond(content)
|
||||
|
||||
async def _send_list(self, evt: MessageEvent) -> None:
|
||||
rooms = await self._list_rooms()
|
||||
rooms = self._sort_rooms(await self._list_rooms())
|
||||
if not rooms:
|
||||
await evt.reply(
|
||||
"De room-index is nog leeg. Moderators kunnen rooms toevoegen met "
|
||||
"`!rooms add <room-id-of-alias>`."
|
||||
)
|
||||
return
|
||||
await self._send_cards(
|
||||
evt, self._build_cards(rooms), "Rooms overzicht", "rooms getoond"
|
||||
)
|
||||
|
||||
def sort_key(r: Dict[str, Any]) -> str:
|
||||
return (
|
||||
r.get("display_name")
|
||||
or r.get("canonical_alias")
|
||||
or r["room_id"]
|
||||
).lower()
|
||||
async def _search_rooms(self, terms: str) -> List[Dict[str, Any]]:
|
||||
"""Zoek rooms: alle termen moeten matchen (case-insensitive, substring)
|
||||
in naam, alias of omschrijving."""
|
||||
needles = [t for t in (part.lower() for part in terms.split()) if t]
|
||||
if not needles:
|
||||
return []
|
||||
found = []
|
||||
for r in await self._list_rooms():
|
||||
hay = " ".join(
|
||||
str(r.get(field) or "").lower()
|
||||
for field in ("display_name", "canonical_alias", "topic")
|
||||
)
|
||||
if all(n in hay for n in needles):
|
||||
found.append(r)
|
||||
return self._sort_rooms(found)
|
||||
|
||||
rooms.sort(key=sort_key)
|
||||
async def _send_search(self, evt: MessageEvent, query: str) -> None:
|
||||
found = await self._search_rooms(query)
|
||||
query_label = " ".join(query.split())
|
||||
if not found:
|
||||
await evt.reply(f"Geen rooms gevonden voor `{query_label}`.")
|
||||
return
|
||||
await self._send_cards(
|
||||
evt,
|
||||
self._build_cards(found),
|
||||
f"Zoekresultaten voor '{query_label}'",
|
||||
"gevonden",
|
||||
)
|
||||
|
||||
lines = []
|
||||
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}"
|
||||
if topic:
|
||||
line += f" — {_escape_md(topic)}"
|
||||
lines.append(line)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
async def _send_help(self, evt: MessageEvent) -> None:
|
||||
"""Uitgebreide Nederlandstalige handleiding voor !rooms."""
|
||||
help_text = (
|
||||
"**!rooms** — overzicht van de room-index\n\n"
|
||||
"Toont alle rooms als kaarten met naam, ledental, omschrijving "
|
||||
"en een directe join-link. Ruimtes met een eigen icoon tonen die "
|
||||
"naast de naam.\n\n"
|
||||
"**Basis:**\n"
|
||||
"* `!rooms` — toon de volledige lijst (bij veel rooms verdeeld "
|
||||
"over meerdere berichten)\n"
|
||||
"* `!rooms over <zoekterm ...>` — zoek rooms op naam, alias en/of "
|
||||
"omschrijving (bijv. `!rooms over linux`); alle termen moeten "
|
||||
"matchen\n\n"
|
||||
"**Subcommando's (moderator of hoger vereist):**\n"
|
||||
"* `!rooms add <room-id of #alias>` — voeg een room toe aan de index\n"
|
||||
"* `!rooms edit <room-id of #alias> <veld>=<waarde>` — wijzig een "
|
||||
"veld van een room\n"
|
||||
"* `!rooms remove <room-id of #alias>` — verwijder een room uit de index\n"
|
||||
"* `!rooms refresh` — controleer of rooms nog bestaan en vul "
|
||||
"ontbrekende avatars aan\n\n"
|
||||
"**Veld bij edit:** `name` (naam), `topic` (omschrijving) of "
|
||||
"`alias` (adres). Een lege waarde wist het veld.\n\n"
|
||||
"**Voorbeelden:**\n"
|
||||
"* `!rooms add #algemeen:mtux.nl`\n"
|
||||
"* `!rooms edit #algemeen:mtux.nl name=Algemeen Overleg`\n"
|
||||
"* `!rooms edit #algemeen:mtux.nl topic=` (wist de omschrijving)\n"
|
||||
"* `!rooms remove #algemeen:mtux.nl`\n\n"
|
||||
"Alleen moderators (macht ≥ 50) mogen rooms toevoegen, wijzigen "
|
||||
"of verwijderen. Iedereen kan de lijst bekijken."
|
||||
)
|
||||
await evt.respond(help_text, markdown=True)
|
||||
|
||||
@command.new(
|
||||
name="rooms",
|
||||
help="Toon de room-index; subcommand: add, remove, refresh",
|
||||
help="Toon de room-index als kaarten; subcommands: over (zoeken), add, edit, remove, refresh, help",
|
||||
require_subcommand=False,
|
||||
)
|
||||
async def handle_rooms(self, evt: MessageEvent) -> None:
|
||||
await evt.mark_read()
|
||||
await self._send_list(evt)
|
||||
|
||||
@handle_rooms.subcommand(name="add", help="Voeg een room toe (moderator+)")
|
||||
@handle_rooms.subcommand(name="help", help="Toon deze handleiding")
|
||||
async def rooms_help(self, evt: MessageEvent) -> None:
|
||||
await self._send_help(evt)
|
||||
|
||||
@handle_rooms.subcommand(
|
||||
name="over",
|
||||
help="Zoek rooms op naam, alias of omschrijving: !rooms over <zoekterm ...>",
|
||||
)
|
||||
@command.argument("terms", pass_raw=True, required=True)
|
||||
async def search_over(self, evt: MessageEvent, terms: str) -> None:
|
||||
if not terms.strip():
|
||||
await evt.reply(
|
||||
"Gebruik: `!rooms over <zoekterm ...>` (bijv. `!rooms over linux`).\n"
|
||||
"Zoekt op naam, alias en omschrijving; alle termen moeten matchen."
|
||||
)
|
||||
return
|
||||
await self._send_search(evt, terms)
|
||||
|
||||
@handle_rooms.subcommand(
|
||||
name="add",
|
||||
help="Voeg een room toe aan de index: !rooms add <room-id of #alias> (moderator+)",
|
||||
)
|
||||
@command.argument("room", pass_raw=True, required=True)
|
||||
async def add_room(self, evt: MessageEvent, room: str) -> None:
|
||||
if not await self._is_mod(evt.room_id, evt.sender):
|
||||
|
|
@ -365,25 +659,82 @@ class RoomsOverviewPlugin(Plugin):
|
|||
return
|
||||
await self.database.execute(
|
||||
"INSERT INTO roomsoverview_rooms "
|
||||
"(room_id, display_name, canonical_alias, topic, member_count, added_by, added_at, last_checked) "
|
||||
"VALUES ($1, $2, $3, $4, $5, $6, $7, $7) "
|
||||
"(room_id, display_name, canonical_alias, topic, member_count, avatar, added_by, added_at, last_checked) "
|
||||
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) "
|
||||
"ON CONFLICT (room_id) DO UPDATE SET display_name = $2, canonical_alias = $3, "
|
||||
"topic = $4, member_count = $5, last_checked = $7",
|
||||
"topic = $4, member_count = $5, avatar = $6, last_checked = $8",
|
||||
room_id,
|
||||
info["name"] or None,
|
||||
info["alias"] or None,
|
||||
info["topic"] or None,
|
||||
info["members"],
|
||||
info["avatar"],
|
||||
evt.sender,
|
||||
int(time.time()),
|
||||
)
|
||||
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+)")
|
||||
@handle_rooms.subcommand(
|
||||
name="edit",
|
||||
help="Wijzig een veld van een room: !rooms edit <room-id of #alias> "
|
||||
"<name|topic|alias>=<waarde> (moderator+)",
|
||||
)
|
||||
@command.argument("args", pass_raw=True, required=True)
|
||||
async def edit_room(self, evt: MessageEvent, args: str) -> None:
|
||||
if not await self._is_mod(evt.room_id, evt.sender):
|
||||
await evt.reply("Geen permissie: moderator of hoger vereist.")
|
||||
return
|
||||
parsed = _parse_edit_args(args)
|
||||
if parsed is None:
|
||||
await evt.reply(
|
||||
"Gebruik: `!rooms edit <room-id-of-alias> <name|topic|alias>=<waarde>`\n"
|
||||
"Lege waarde wist het veld (bijv. `alias=`)."
|
||||
)
|
||||
return
|
||||
room_ref, field, value = parsed
|
||||
field_ok = {"name": "display_name", "topic": "topic", "alias": "canonical_alias"}
|
||||
if field not in field_ok:
|
||||
await evt.reply("Veld moet `name`, `topic` of `alias` zijn.")
|
||||
return
|
||||
if field == "alias" and value and not value.startswith("#"):
|
||||
await evt.reply("Alias moet beginnen met `#` (bijv. `#mijn-room:server.nl`).")
|
||||
return
|
||||
room_id = room_ref if room_ref.startswith("!") else await self._resolve_alias(room_ref)
|
||||
if not room_id:
|
||||
await evt.reply(f"Room `{room_ref}` kon niet gevonden worden.")
|
||||
return
|
||||
new_value = value or None # lege waarde wist het veld (NULL)
|
||||
column = field_ok[field]
|
||||
result = await self.database.execute(
|
||||
f"UPDATE roomsoverview_rooms SET {column} = $2, last_checked = $3 "
|
||||
"WHERE room_id = $1",
|
||||
room_id,
|
||||
new_value,
|
||||
int(time.time()),
|
||||
)
|
||||
if result != "UPDATE 1":
|
||||
await evt.reply(f"Room `{room_id}` stond niet in de index.")
|
||||
return
|
||||
row = await self.database.fetchrow(
|
||||
"SELECT display_name, canonical_alias FROM roomsoverview_rooms "
|
||||
"WHERE room_id = $1",
|
||||
room_id,
|
||||
)
|
||||
label = row["display_name"] or row["canonical_alias"] or room_id
|
||||
shown = new_value if new_value is not None else "(leeg)"
|
||||
field_name = {"name": "naam", "topic": "omschrijving", "alias": "alias"}[field]
|
||||
await evt.reply(
|
||||
f"Room bijgewerkt: **{label}** — {field_name} is nu `{shown}`."
|
||||
)
|
||||
|
||||
@handle_rooms.subcommand(
|
||||
name="remove",
|
||||
help="Verwijder een room uit de index: !rooms remove <room-id of #alias> (moderator+)",
|
||||
)
|
||||
@command.argument("room", pass_raw=True, required=True)
|
||||
async def remove_room(self, evt: MessageEvent, room: str) -> None:
|
||||
if not await self._is_mod(evt.room_id, evt.sender):
|
||||
|
|
@ -401,13 +752,18 @@ class RoomsOverviewPlugin(Plugin):
|
|||
else:
|
||||
await evt.reply(f"Room `{room_id}` stond niet in de index.")
|
||||
|
||||
@handle_rooms.subcommand(name="refresh", help="Valideer de index nu (moderator+)")
|
||||
@handle_rooms.subcommand(
|
||||
name="refresh",
|
||||
help="Controleer of rooms bestaan en vul ontbrekende avatars aan (moderator+)",
|
||||
)
|
||||
async def refresh_index(self, evt: MessageEvent) -> None:
|
||||
if not await self._is_mod(evt.room_id, evt.sender):
|
||||
await evt.reply("Geen permissie: moderator of hoger vereist.")
|
||||
return
|
||||
removed, total = await self._validate_all()
|
||||
filled = await self._enrich_avatars_online()
|
||||
await evt.reply(
|
||||
f"Index gecontroleerd: {total - removed} rooms over, "
|
||||
f"{removed} verwijderd (bestonden niet meer)."
|
||||
f"{removed} verwijderd (bestonden niet meer). "
|
||||
f"{filled} avatar(s) aangevuld."
|
||||
)
|
||||
Loading…
Reference in a new issue