Implement external calendar integration and enhance API functionality
- Added support for fetching and parsing external ICS calendars, allowing events to be displayed on the duty grid. - Introduced a new API endpoint `/api/calendar-events` to retrieve calendar events within a specified date range. - Updated configuration to include `EXTERNAL_CALENDAR_ICS_URL` for specifying the ICS calendar URL. - Enhanced the web application to visually indicate days with events and provide event summaries on hover. - Improved documentation in the README to include details about the new calendar integration and configuration options. - Updated tests to cover the new calendar functionality and ensure proper integration.
This commit is contained in:
43
api/app.py
43
api/app.py
@@ -11,8 +11,9 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from db.session import session_scope
|
||||
from db.repository import get_duties
|
||||
from db.schemas import DutyWithUser
|
||||
from db.schemas import DutyWithUser, CalendarEvent
|
||||
from api.telegram_auth import validate_init_data_with_reason
|
||||
from api.calendar_ics import get_calendar_events
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -132,6 +133,46 @@ def list_duties(
|
||||
return _fetch_duties_response(from_date, to_date)
|
||||
|
||||
|
||||
def _require_same_auth(
|
||||
request: Request,
|
||||
x_telegram_init_data: str | None,
|
||||
) -> None:
|
||||
"""Raise HTTPException 403 if not allowed (same logic as list_duties)."""
|
||||
init_data = (x_telegram_init_data or "").strip()
|
||||
if not init_data:
|
||||
client_host = request.client.host if request.client else None
|
||||
if _is_private_client(client_host) or config.MINI_APP_SKIP_AUTH:
|
||||
return
|
||||
log.warning("calendar-events: no X-Telegram-Init-Data header (client=%s)", client_host)
|
||||
raise HTTPException(status_code=403, detail="Откройте календарь из Telegram")
|
||||
max_age = config.INIT_DATA_MAX_AGE_SECONDS or None
|
||||
username, auth_reason = validate_init_data_with_reason(
|
||||
init_data, config.BOT_TOKEN, max_age_seconds=max_age
|
||||
)
|
||||
if username is None:
|
||||
log.warning("calendar-events: initData validation failed: %s", auth_reason)
|
||||
raise HTTPException(status_code=403, detail=_auth_error_detail(auth_reason))
|
||||
if not config.can_access_miniapp(username):
|
||||
log.warning("calendar-events: username not in allowlist")
|
||||
raise HTTPException(status_code=403, detail="Доступ запрещён")
|
||||
|
||||
|
||||
@app.get("/api/calendar-events", response_model=list[CalendarEvent])
|
||||
def list_calendar_events(
|
||||
request: Request,
|
||||
from_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="from"),
|
||||
to_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="to"),
|
||||
x_telegram_init_data: str | None = Header(None, alias="X-Telegram-Init-Data"),
|
||||
) -> list[CalendarEvent]:
|
||||
_validate_duty_dates(from_date, to_date)
|
||||
_require_same_auth(request, x_telegram_init_data)
|
||||
url = config.EXTERNAL_CALENDAR_ICS_URL
|
||||
if not url:
|
||||
return []
|
||||
events = get_calendar_events(url, from_date=from_date, to_date=to_date)
|
||||
return [CalendarEvent(date=e["date"], summary=e["summary"]) for e in events]
|
||||
|
||||
|
||||
webapp_path = Path(__file__).resolve().parent.parent / "webapp"
|
||||
if webapp_path.is_dir():
|
||||
app.mount("/app", StaticFiles(directory=str(webapp_path), html=True), name="webapp")
|
||||
|
||||
Reference in New Issue
Block a user