From 0118c68690bfbc37bfa6c620b50aa681bacc7f1b Mon Sep 17 00:00:00 2001 From: Christoph Sterz Date: Sun, 1 Nov 2026 12:12:22 +0100 Subject: [PATCH] Initial Commit for testing --- PKGBUILD | 23 ++++++++++++++ matrix-status-bot-set@.service | 10 +++++++ matrix-status-bot.conf | 14 +++++++++ matrix-status-bot.service | 19 ++++++++++++ status.py | 55 ++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 PKGBUILD create mode 100644 matrix-status-bot-set@.service create mode 100644 matrix-status-bot.conf create mode 100644 matrix-status-bot.service create mode 100644 status.py diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..87ee156 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,23 @@ +pkgname=matrix-status-bot +pkgver=0.1.0 +pkgrel=1 +pkgdesc="Mirror a makerspace's open/closed state into a Matrix room name and avatar" +arch=(any) +license=(MIT) +depends=(python) +backup=(etc/matrix-status-bot.conf) +source=(status.py + matrix-status-bot.service + matrix-status-bot-set@.service + matrix-status-bot.conf) +sha256sums=('42b130bfd22dad0ab03335b4774db4a9413581bc8e4e21792228c88a6bf61f30' + '019a9b6a728cc1bb0a9f674c3cbbd9b2b561d6d40355bdb80413f7ab9e6f55d7' + '9fbfb7f67f7eb747c638749da99fa1f3c356c765ae228e6f9605028ee7673a6b' + 'c53c003089c47087efd24a1d9abcf8b1bd69e391f58093a45a18a6370c3784f6') + +package() { + install -Dm755 status.py "$pkgdir/usr/bin/matrix-status-bot" + install -Dm644 -t "$pkgdir/usr/lib/systemd/system" \ + matrix-status-bot.service matrix-status-bot-set@.service + install -Dm600 matrix-status-bot.conf "$pkgdir/etc/matrix-status-bot.conf" +} diff --git a/matrix-status-bot-set@.service b/matrix-status-bot-set@.service new file mode 100644 index 0000000..9299acb --- /dev/null +++ b/matrix-status-bot-set@.service @@ -0,0 +1,10 @@ +[Unit] +Description=Set Matrix makerspace status to %i +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +EnvironmentFile=/etc/matrix-status-bot.conf +ExecStart=/usr/bin/matrix-status-bot %i +DynamicUser=yes diff --git a/matrix-status-bot.conf b/matrix-status-bot.conf new file mode 100644 index 0000000..3733c55 --- /dev/null +++ b/matrix-status-bot.conf @@ -0,0 +1,14 @@ +# matrix-status-bot configuration (read by systemd as root; keep mode 600) +MATRIX_HS=https://matrix.example.org +MATRIX_TOKEN= +MATRIX_ROOM=!roomid:example.org + +# Used by matrix-status-bot.service (polling mode) +SPACEAPI_URL=https://example.org/spaceapi.json +POLL_INTERVAL=60 + +# Optional +#NAME_OPEN=🟢 Space is open +#NAME_CLOSED=🔴 Space is closed +#AVATAR_OPEN=mxc://example.org/green +#AVATAR_CLOSED=mxc://example.org/red diff --git a/matrix-status-bot.service b/matrix-status-bot.service new file mode 100644 index 0000000..e5572a3 --- /dev/null +++ b/matrix-status-bot.service @@ -0,0 +1,19 @@ +[Unit] +Description=Matrix makerspace status bot (SpaceAPI -> room name/avatar) +Wants=network-online.target +After=network-online.target + +[Service] +EnvironmentFile=/etc/matrix-status-bot.conf +ExecStart=/usr/bin/matrix-status-bot --spaceapi $SPACEAPI_URL $POLL_INTERVAL +Restart=on-failure +RestartSec=30 +DynamicUser=yes +ProtectHome=yes +PrivateDevices=yes +CapabilityBoundingSet= +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallFilter=@system-service + +[Install] +WantedBy=multi-user.target diff --git a/status.py b/status.py new file mode 100644 index 0000000..acfe6dd --- /dev/null +++ b/status.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Minimal Matrix status-room bot: mirrors open/closed into a room's name (and avatar). + +Usage: + status.py open|closed set once (door switch, button, cron, ...) + status.py --spaceapi URL [SECS] poll a SpaceAPI endpoint every SECS seconds (default 60) + +Env: MATRIX_HS (https://matrix.example.org), MATRIX_TOKEN, MATRIX_ROOM (!id:server) +Optional: NAME_OPEN / NAME_CLOSED, AVATAR_OPEN / AVATAR_CLOSED (mxc:// URIs) +""" +import json, os, sys, time, urllib.error, urllib.parse, urllib.request + +HS, TOKEN, ROOM = (os.environ[k] for k in ("MATRIX_HS", "MATRIX_TOKEN", "MATRIX_ROOM")) +NAMES = {True: os.getenv("NAME_OPEN", "🟢 Space is open"), False: os.getenv("NAME_CLOSED", "🔴 Space is closed")} +AVATARS = {True: os.getenv("AVATAR_OPEN"), False: os.getenv("AVATAR_CLOSED")} + + +def state(method, event_type, body=None): + url = f"{HS.rstrip('/')}/_matrix/client/v3/rooms/{urllib.parse.quote(ROOM, safe='')}/state/{event_type}" + req = urllib.request.Request(url, method=method, data=body and json.dumps(body).encode(), + headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=10) as r: + return json.load(r) + + +def set_status(is_open): + try: + if state("GET", "m.room.name").get("name") == NAMES[is_open]: + return # unchanged: don't add a "changed the room name" line + except urllib.error.HTTPError as e: + if e.code != 404: # 404 = room has no name yet + raise + state("PUT", "m.room.name", {"name": NAMES[is_open]}) + if AVATARS[is_open]: + state("PUT", "m.room.avatar", {"url": AVATARS[is_open]}) + + +if __name__ == "__main__": + args = sys.argv[1:] + if args in (["open"], ["closed"]): + set_status(args[0] == "open") + elif args[:1] == ["--spaceapi"] and len(args) in (2, 3): + last = None + while True: + try: + with urllib.request.urlopen(args[1], timeout=10) as r: + is_open = bool(json.load(r)["state"]["open"]) + if is_open != last: + set_status(is_open) + last = is_open + except Exception as e: + print("error:", e, file=sys.stderr) + time.sleep(int(args[2]) if len(args) == 3 else 60) + else: + sys.exit(__doc__)