New apps Added
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user