feat: Add Matrix notifications
Send Matrix alerts for updates and errors, and document the required bot credentials.
This commit is contained in:
parent
7c778e338c
commit
4ab799c156
4 changed files with 107 additions and 3 deletions
|
|
@ -7,3 +7,8 @@ GITHUB_TOKEN=optional-github-token
|
||||||
# FreshRSS login for authenticated About page
|
# FreshRSS login for authenticated About page
|
||||||
FRESHRSS_USERNAME=your-username
|
FRESHRSS_USERNAME=your-username
|
||||||
FRESHRSS_PASSWORD=your-password
|
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
|
||||||
|
|
|
||||||
|
|
@ -125,3 +125,12 @@ Acceptance criteria:
|
||||||
- The script can log in to FreshRSS using credentials from environment variables.
|
- The script can log in to FreshRSS using credentials from environment variables.
|
||||||
- FreshRSS version is extracted from the About page after authentication.
|
- FreshRSS version is extracted from the About page after authentication.
|
||||||
- `.env.sample` documents the FreshRSS credentials required.
|
- `.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.
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,15 @@ Copy `.env.sample` to `.env` and fill required values. Export the variables befo
|
||||||
export PAPERLESS_API_TOKEN=...
|
export PAPERLESS_API_TOKEN=...
|
||||||
export FRESHRSS_USERNAME=...
|
export FRESHRSS_USERNAME=...
|
||||||
export FRESHRSS_PASSWORD=...
|
export FRESHRSS_PASSWORD=...
|
||||||
|
export MATRIX_HOMESERVER=...
|
||||||
|
export MATRIX_ROOM_ID=...
|
||||||
|
export MATRIX_ACCESS_TOKEN=...
|
||||||
```
|
```
|
||||||
|
|
||||||
The script also reads `.env` automatically if present.
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from http.cookiejar import CookieJar
|
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
|
from urllib.request import HTTPCookieProcessor, Request, build_opener, urlopen
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
@ -483,6 +484,87 @@ def check_service(service: ServiceConfig, timeout: float, user_agent: str) -> Di
|
||||||
return result
|
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:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Check for webservice updates")
|
parser = argparse.ArgumentParser(description="Check for webservice updates")
|
||||||
parser.add_argument("--config", default="services.yaml", help="Path to services YAML")
|
parser.add_argument("--config", default="services.yaml", help="Path to services YAML")
|
||||||
|
|
@ -512,7 +594,6 @@ def main() -> int:
|
||||||
continue
|
continue
|
||||||
current = result["current"] or "unknown"
|
current = result["current"] or "unknown"
|
||||||
latest = result["latest"] or "unknown"
|
latest = result["latest"] or "unknown"
|
||||||
upstream = result["upstream_url"] or "unknown"
|
|
||||||
notes = []
|
notes = []
|
||||||
if comparison is None and result["current"] and result["latest"]:
|
if comparison is None and result["current"] and result["latest"]:
|
||||||
notes.append("unparseable")
|
notes.append("unparseable")
|
||||||
|
|
@ -521,7 +602,7 @@ def main() -> int:
|
||||||
if result["latest_error"]:
|
if result["latest_error"]:
|
||||||
notes.append("latest error")
|
notes.append("latest error")
|
||||||
suffix = f" [{' '.join(notes)}]" if notes else ""
|
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:
|
if output_lines:
|
||||||
print("\n".join(output_lines))
|
print("\n".join(output_lines))
|
||||||
|
|
@ -537,6 +618,10 @@ def main() -> int:
|
||||||
f"{result['name']}: latest version error: {result['latest_error']}",
|
f"{result['name']}: latest version error: {result['latest_error']}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
summary, should_notify = build_summary(results)
|
||||||
|
if should_notify:
|
||||||
|
send_matrix_message(summary, args.timeout)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue