mirror of
https://github.com/wizarrrr/wizarr.git
synced 2026-07-30 23:07:19 -04:00
- 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
167 lines
4.7 KiB
Python
167 lines
4.7 KiB
Python
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,
|
||
)
|