77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""Token-gated ICS calendar feed view (FEAT-10).
|
|
|
|
A calendar app subscribing to a URL can't send an HA bearer token, so this view
|
|
is public-by-URL and authenticated by an unguessable per-instance token in the
|
|
``?token=`` query param (compared in constant time). It serves a read-only feed
|
|
of upcoming chores; no mutation is possible.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hmac
|
|
import logging
|
|
from datetime import timedelta
|
|
from http import HTTPStatus
|
|
|
|
from aiohttp import web
|
|
from homeassistant.components.http import HomeAssistantView
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from . import ics
|
|
from .const import DOMAIN
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CALENDAR_URL = "/api/taskmate/calendar.ics"
|
|
HTTP_CAL_REGISTERED = "calendar_http_registered"
|
|
|
|
|
|
def _get_coordinator(hass: HomeAssistant):
|
|
from .coordinator import TaskMateCoordinator
|
|
for value in hass.data.get(DOMAIN, {}).values():
|
|
if isinstance(value, TaskMateCoordinator):
|
|
return value
|
|
return None
|
|
|
|
|
|
class TaskMateCalendarFeedView(HomeAssistantView):
|
|
"""Serve an ICS feed of upcoming chores, gated by the instance token."""
|
|
|
|
url = CALENDAR_URL
|
|
name = "api:taskmate:calendar"
|
|
requires_auth = False
|
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
|
self.hass = hass
|
|
|
|
async def get(self, request: web.Request) -> web.Response:
|
|
coordinator = _get_coordinator(self.hass)
|
|
if coordinator is None:
|
|
return web.Response(status=HTTPStatus.NOT_FOUND)
|
|
|
|
expected = coordinator.storage.get_setting("ics_token", "")
|
|
supplied = request.query.get("token", "")
|
|
if not expected or not supplied or not hmac.compare_digest(str(expected), supplied):
|
|
return web.Response(status=HTTPStatus.UNAUTHORIZED)
|
|
|
|
days = coordinator._calendar_projection_days()
|
|
start = dt_util.now().date()
|
|
end = start + timedelta(days=days)
|
|
events = ics.build_chore_events(coordinator, start, end)
|
|
body = ics.build_calendar(events, dt_util.now())
|
|
return web.Response(
|
|
body=body.encode("utf-8"),
|
|
content_type="text/calendar",
|
|
charset="utf-8",
|
|
headers={"Cache-Control": "private, max-age=3600"},
|
|
)
|
|
|
|
|
|
def async_register_calendar_view(hass: HomeAssistant) -> None:
|
|
"""Register the ICS feed view once."""
|
|
if hass.data.get(DOMAIN, {}).get(HTTP_CAL_REGISTERED):
|
|
return
|
|
hass.http.register_view(TaskMateCalendarFeedView(hass))
|
|
hass.data.setdefault(DOMAIN, {})[HTTP_CAL_REGISTERED] = True
|
|
_LOGGER.debug("Registered TaskMate calendar ICS view")
|