feat: Add Matrix notifications

Send Matrix alerts for updates and errors, and document the required bot credentials.
This commit is contained in:
Jeena 2026-03-12 15:23:45 +00:00
parent 7c778e338c
commit 4ab799c156
4 changed files with 107 additions and 3 deletions

View file

@ -7,3 +7,8 @@ GITHUB_TOKEN=optional-github-token
# FreshRSS login for authenticated About page
FRESHRSS_USERNAME=your-username
FRESHRSS_PASSWORD=your-password
# Matrix bot notifications
MATRIX_HOMESERVER=https://matrix.example.net
MATRIX_ROOM_ID=!roomid:example.net
MATRIX_ACCESS_TOKEN=replace-with-access-token

View file

@ -125,3 +125,12 @@ Acceptance criteria:
- The script can log in to FreshRSS using credentials from environment variables.
- FreshRSS version is extracted from the About page after authentication.
- `.env.sample` documents the FreshRSS credentials required.
## US-14 - Matrix Notifications
As a maintainer, I want the script to post update alerts to a Matrix room so that I can track service changes and failures without email.
Acceptance criteria:
- The script posts a message to a Matrix room when updates are detected or errors occur.
- Matrix credentials (homeserver, room id, access token) are read from `.env`.
- The message includes actionable service names and version details.

View file

@ -22,10 +22,15 @@ Copy `.env.sample` to `.env` and fill required values. Export the variables befo
export PAPERLESS_API_TOKEN=...
export FRESHRSS_USERNAME=...
export FRESHRSS_PASSWORD=...
export MATRIX_HOMESERVER=...
export MATRIX_ROOM_ID=...
export MATRIX_ACCESS_TOKEN=...
```
The script also reads `.env` automatically if present.
The Matrix bot will attempt to join the configured room automatically if it is not already a member.
## Usage
```bash

View file

@ -4,10 +4,11 @@ import json
import os
import re
import sys
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional
from http.cookiejar import CookieJar
from urllib.parse import urlencode
from urllib.parse import urlencode, quote
from urllib.request import HTTPCookieProcessor, Request, build_opener, urlopen
import bcrypt
@ -483,6 +484,87 @@ def check_service(service: ServiceConfig, timeout: float, user_agent: str) -> Di
return result
def build_summary(results: list[Dict[str, Any]]) -> tuple[str, bool]:
updates = []
errors = []
for result in results:
comparison = compare_versions(result["current"], result["latest"])
if comparison == 1:
updates.append(result)
if result["current_error"] or result["latest_error"]:
errors.append(result)
lines = []
if updates:
lines.append("Updates available:")
for result in updates:
current = result["current"] or "unknown"
latest = result["latest"] or "unknown"
lines.append(f"- {result['name']}: {current} -> {latest}")
if errors:
lines.append("Errors:")
for result in errors:
if result["current_error"]:
lines.append(f"- {result['name']}: current version error")
if result["latest_error"]:
lines.append(f"- {result['name']}: latest version error")
if not lines:
return "All services up to date.", False
return "\n".join(lines), True
def send_matrix_message(message: str, timeout: float) -> None:
homeserver = os.getenv("MATRIX_HOMESERVER")
room_id = os.getenv("MATRIX_ROOM_ID")
token = os.getenv("MATRIX_ACCESS_TOKEN")
if not homeserver or not room_id or not token:
return
join_matrix_room(homeserver, room_id, token, timeout)
txn_id = str(int(time.time() * 1000))
url = f"{homeserver}/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txn_id}"
payload = json.dumps({
"msgtype": "m.text",
"body": message,
}).encode("utf-8")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
try:
fetch_response(
url,
timeout=timeout,
user_agent="check-for-updates",
extra_headers=headers,
data=payload,
method="PUT",
)
except Exception as exc:
print(f"Matrix notification failed: {exc}", file=sys.stderr)
def join_matrix_room(homeserver: str, room_id: str, token: str, timeout: float) -> None:
encoded_room = quote(room_id, safe="")
url = f"{homeserver}/_matrix/client/v3/join/{encoded_room}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
try:
fetch_response(
url,
timeout=timeout,
user_agent="check-for-updates",
extra_headers=headers,
data=b"{}",
method="POST",
)
except Exception:
return
def main() -> int:
parser = argparse.ArgumentParser(description="Check for webservice updates")
parser.add_argument("--config", default="services.yaml", help="Path to services YAML")
@ -512,7 +594,6 @@ def main() -> int:
continue
current = result["current"] or "unknown"
latest = result["latest"] or "unknown"
upstream = result["upstream_url"] or "unknown"
notes = []
if comparison is None and result["current"] and result["latest"]:
notes.append("unparseable")
@ -521,7 +602,7 @@ def main() -> int:
if result["latest_error"]:
notes.append("latest error")
suffix = f" [{' '.join(notes)}]" if notes else ""
output_lines.append(f"{result['name']}: {current} -> {latest} ({upstream}){suffix}")
output_lines.append(f"{result['name']}: {current} -> {latest}{suffix}")
if output_lines:
print("\n".join(output_lines))
@ -537,6 +618,10 @@ def main() -> int:
f"{result['name']}: latest version error: {result['latest_error']}",
file=sys.stderr,
)
summary, should_notify = build_summary(results)
if should_notify:
send_matrix_message(summary, args.timeout)
return 0