Files
wizarr/app/services/notifications.py
Matthieu B 73c29458fe fix: resolve all test failures, linting errors, and type errors
- Fix 55 test failures caused by missing request contexts and incorrect
  session_transaction() usage across 8 test files
- Fix ruff import sorting errors and unused imports
- Fix 122 type errors: rename method override parameters to match base
  classes, add None guards for fetchone()/datetime, widen dict type
  annotations, add type: ignore for SQLAlchemy stub limitations
- Add [tool.ty.rules] config to suppress unsupported-base warnings
- Fix _ variable shadowing gettext in wizard routes
- Add noqa: ARG002 for unused method arguments required by base class
2026-03-29 16:20:23 +02:00

167 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import base64
import json
import logging
from typing import Any
import apprise
import requests
from app.models import Notification
__all__ = ["notify"]
def _send(url: str, data, headers: dict) -> bool:
try:
resp = requests.post(url, data=data, headers=headers, timeout=5)
if resp.status_code in (200, 204):
return True
logging.error(
"Notification failed %s%s%s", url, resp.status_code, resp.text
)
return False
except requests.RequestException as exc:
logging.error("Notification request error: %s", exc)
return False
def _discord(
msg: str,
webhook_url: str,
title: str = "Wizarr Notification",
previous_version: str | None = None,
new_version: str | None = None,
) -> bool:
embed: dict[str, Any] = {
"title": title,
"description": msg,
"author": {
"name": "Wizarr",
"icon_url": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/wizarr.png",
},
}
# Add version fields for update notifications
if previous_version and new_version:
embed["fields"] = [
{"name": "Previous Version", "value": previous_version, "inline": True},
{"name": "New Version", "value": new_version, "inline": True},
]
data = json.dumps({"embeds": [embed]})
headers = {"Content-Type": "application/json"}
return _send(webhook_url, data, headers)
def _ntfy(
msg: str,
title: str,
tags: str,
url: str,
username: str | None,
password: str | None,
) -> bool:
headers = {"Title": title, "Tags": tags}
if username and password:
creds = f"{username}:{password}"
headers["Authorization"] = "Basic " + base64.b64encode(creds.encode()).decode()
return _send(url, msg, headers)
def _apprise(msg: str, title: str, _tags: str, url: str) -> bool:
try:
apprise_client = apprise.Apprise()
apprise_client.add(url)
result = apprise_client.notify(title=title, body=msg)
logging.info(f"Apprise notification {'sent' if result else 'failed'}: {title}")
return bool(result)
except Exception as e:
logging.error(f"Error sending Apprise notification: {e}")
return False
def _notifiarr(
msg: str,
title: str,
url: str,
channel_id: int,
) -> bool:
data = json.dumps(
{
"notification": {"update": False, "name": "Wizarr", "event": ""},
"discord": {
"color": "FFFFFF",
"ping": {"pingUser": 0, "pingRole": 0},
"images": {"thumbnail": "", "image": ""},
"text": {
"title": title,
"icon": "https://raw.githubusercontent.com/wizarrrr/wizarr/refs/heads/main/app/static/img/pwa-icons/icon-128x128.png",
"content": "",
"description": msg,
"fields": [],
"footer": "",
},
"ids": {"channel": channel_id},
},
}
)
headers = {"Content-Type": "application/json"}
return _send(url, data, headers)
def _telegram(
msg: str,
title: str,
baseUrl: str,
bot_token: str,
chat_id: str,
) -> bool:
baseUrl = baseUrl.rstrip("/")
url = f"{baseUrl}/bot{bot_token}/sendMessage"
data = json.dumps(
{
"chat_id": chat_id,
"text": f"{title}\n{msg}",
}
)
headers = {"Content-Type": "application/json"}
return _send(url, data, headers)
def notify(
title: str,
message: str,
tags: str,
event_type: str = "user_joined",
previous_version: str | None = None,
new_version: str | None = None,
):
"""Broadcast to every configured agent that is subscribed to the event type."""
for agent in Notification.query.all():
# Check if agent is subscribed to this event type
subscribed_events = (
agent.notification_events.split(",") if agent.notification_events else []
)
if event_type not in subscribed_events:
continue
if agent.type == "discord":
_discord(message, agent.url, title, previous_version, new_version)
elif agent.type == "ntfy":
_ntfy(message, title, tags, agent.url, agent.username, agent.password)
elif agent.type == "apprise":
_apprise(message, title, tags, agent.url)
elif agent.type == "notifiarr":
_notifiarr(message, title, agent.url, agent.channel_id)
elif agent.type == "telegram":
_telegram(
message,
title,
agent.url,
agent.telegram_bot_token,
agent.telegram_chat_id,
)