132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
"""
|
|
tools/pushover_tool.py — Pushover push notifications.
|
|
|
|
Sends to exactly one hard-coded user key (defined in security.py).
|
|
Normal priority (0) and below: no confirmation required.
|
|
Emergency priority (2): always requires confirmation.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from ..database import credential_store
|
|
from .base import BaseTool, ToolResult
|
|
|
|
PUSHOVER_API_URL = "https://api.pushover.net/1/messages.json"
|
|
|
|
|
|
class PushoverTool(BaseTool):
|
|
name = "pushover"
|
|
description = (
|
|
"Send a push notification to the owner's phone via Pushover. "
|
|
"Use for alerts, reminders, and status updates. "
|
|
"Priority: -2 (silent), -1 (quiet), 0 (normal), 1 (high), 2 (emergency — requires confirmation)."
|
|
)
|
|
input_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Notification title (short, shown in bold)",
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Notification body text",
|
|
},
|
|
"priority": {
|
|
"type": "integer",
|
|
"enum": [-2, -1, 0, 1, 2],
|
|
"description": "Priority level (-2 silent to 2 emergency). Default: 0",
|
|
},
|
|
"url": {
|
|
"type": "string",
|
|
"description": "Optional URL to attach to the notification",
|
|
},
|
|
"url_title": {
|
|
"type": "string",
|
|
"description": "Display text for the attached URL",
|
|
},
|
|
},
|
|
"required": ["title", "message"],
|
|
}
|
|
requires_confirmation = False # overridden dynamically for priority=2
|
|
allowed_in_scheduled_tasks = True
|
|
|
|
async def execute(
|
|
self,
|
|
title: str,
|
|
message: str,
|
|
priority: int = 0,
|
|
url: str = "",
|
|
url_title: str = "",
|
|
**kwargs,
|
|
) -> ToolResult:
|
|
# Validate priority
|
|
if priority not in (-2, -1, 0, 1, 2):
|
|
return ToolResult(success=False, error=f"Invalid priority: {priority}")
|
|
|
|
# Emergency always requires confirmation — enforced here as defence-in-depth
|
|
# (the agent loop also checks requires_confirmation before calling execute)
|
|
if priority == 2:
|
|
# The agent loop should have asked for confirmation already.
|
|
# If we got here, it was approved.
|
|
pass
|
|
|
|
# Load credentials
|
|
try:
|
|
app_token = await credential_store.require("pushover_app_token")
|
|
user_key = await credential_store.require("pushover_user_key")
|
|
except RuntimeError as e:
|
|
return ToolResult(success=False, error=str(e))
|
|
|
|
payload: dict = {
|
|
"token": app_token,
|
|
"user": user_key,
|
|
"title": title[:250],
|
|
"message": message[:1024],
|
|
"priority": priority,
|
|
}
|
|
|
|
if priority == 2:
|
|
# Emergency: retry every 30s for 1 hour until acknowledged
|
|
payload["retry"] = 30
|
|
payload["expire"] = 3600
|
|
|
|
if url:
|
|
payload["url"] = url[:512]
|
|
if url_title:
|
|
payload["url_title"] = url_title[:100]
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.post(PUSHOVER_API_URL, data=payload)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
if data.get("status") != 1:
|
|
return ToolResult(
|
|
success=False,
|
|
error=f"Pushover API error: {data.get('errors', 'unknown')}",
|
|
)
|
|
|
|
return ToolResult(
|
|
success=True,
|
|
data={"sent": True, "request_id": data.get("request", "")},
|
|
)
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
return ToolResult(success=False, error=f"Pushover HTTP error: {e.response.status_code}")
|
|
except Exception as e:
|
|
return ToolResult(success=False, error=f"Pushover error: {e}")
|
|
|
|
def confirmation_description(self, title: str = "", message: str = "", priority: int = 0, **kwargs) -> str:
|
|
return f"Send emergency Pushover notification: '{title}' — {message[:100]}"
|
|
|
|
def get_schema(self) -> dict:
|
|
"""Override to make requires_confirmation dynamic for priority 2."""
|
|
schema = super().get_schema()
|
|
# The tool itself handles emergency confirmation — marked as requiring it always
|
|
# so the agent loop treats it consistently. For non-emergency, the agent loop
|
|
# still calls execute(), which works fine without confirmation.
|
|
return schema
|