diff --git a/VERSION b/VERSION index 3db58c9..9423d08 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.8.19 +2.8.20 diff --git a/app/dashboard/backend/mission_router.py b/app/dashboard/backend/mission_router.py index e9d3ab7..0c82376 100644 --- a/app/dashboard/backend/mission_router.py +++ b/app/dashboard/backend/mission_router.py @@ -1,6 +1,7 @@ import json import logging import io +import re import time from datetime import datetime from typing import Any, Dict, Optional @@ -25,6 +26,7 @@ class MissionCallEvent(BaseModel): call_id: str = Field(..., min_length=1, max_length=128) caller_number: Optional[str] = None queue_name: Optional[str] = None + recipient_extension: Optional[str] = None timestamp: Optional[datetime] = None @@ -157,6 +159,7 @@ def _event_from_query(request: Request) -> MissionCallEvent: call_id=call_id, caller_number=_first_query_param(request, "caller_number", "caller", "from", "number", "phone"), queue_name=_first_query_param(request, "queue_name", "queue", "group", "line"), + recipient_extension=_first_query_param(request, "recipient_extension", "extension", "active_user", "local"), timestamp=_parse_query_timestamp(request), ) @@ -167,15 +170,24 @@ def _get_webhook_token() -> str: return db_token.strip() or env_token -def _validate_mission_webhook_token(request: Request, token: Optional[str] = None) -> None: +def _validate_mission_webhook_token( + request: Request, + token: Optional[str] = None, + *, + allow_telefoni_token: bool = False, +) -> None: configured = _get_webhook_token() + telefoni_token = (MissionService.get_setting_value("telefoni_shared_secret", "") or "").strip() + if not telefoni_token: + telefoni_token = (getattr(settings, "TELEFONI_SHARED_SECRET", "") or "").strip() path = request.url.path - if not configured: + accepted_tokens = {value for value in (configured, telefoni_token if allow_telefoni_token else "") if value} + if not accepted_tokens: logger.warning("❌ Mission webhook rejected path=%s reason=token_not_configured", path) raise HTTPException(status_code=403, detail="Mission webhook token not configured") candidate = token or request.headers.get("x-mission-token") or request.query_params.get("token") - if not candidate or candidate.strip() != configured: + if not candidate or candidate.strip() not in accepted_tokens: source = "query_or_arg" if not token and request.headers.get("x-mission-token"): source = "header" @@ -194,6 +206,25 @@ def _validate_mission_webhook_token(request: Request, token: Optional[str] = Non raise HTTPException(status_code=403, detail="Forbidden") +def _display_destination(event: MissionCallEvent) -> str: + """Return the person/phone receiving a Yealink incoming call.""" + raw = (event.recipient_extension or "").strip() + match = re.search(r"(?:sip:)?(\d+)", raw, flags=re.IGNORECASE) + extension = match.group(1) if match else "" + if extension: + user = execute_query_single( + """SELECT COALESCE(NULLIF(TRIM(full_name), ''), NULLIF(TRIM(username), '')) AS name + FROM users + WHERE telefoni_aktiv = TRUE AND telefoni_extension = %s + ORDER BY user_id LIMIT 1""", + (extension,), + ) + if user and user.get("name"): + return f"{user['name']} ({extension})" + return f"Telefon {extension}" + return (event.queue_name or "Ukendt telefon").strip() + + def _normalize_uptime_payload(payload: MissionUptimeWebhook) -> Dict[str, Any]: raw = dict(payload.payload or {}) @@ -689,7 +720,7 @@ async def mission_ws(websocket: WebSocket): @router.post("/mission/webhook/telefoni/ringing") async def mission_telefoni_ringing(event: MissionCallEvent, request: Request, token: Optional[str] = Query(None)): - _validate_mission_webhook_token(request, token) + _validate_mission_webhook_token(request, token, allow_telefoni_token=True) logger.info( "☎️ Mission webhook ringing call_id=%s caller=%s queue=%s method=%s", @@ -701,7 +732,7 @@ async def mission_telefoni_ringing(event: MissionCallEvent, request: Request, to timestamp = event.timestamp or datetime.utcnow() context = MissionService.resolve_contact_context(event.caller_number) - queue_name = (event.queue_name or "Ukendt kø").strip() + queue_name = _display_destination(event) execute_query( """ @@ -754,6 +785,7 @@ async def mission_telefoni_ringing(event: MissionCallEvent, request: Request, to "call_id": event.call_id, "queue_name": queue_name, "caller_number": event.caller_number, + "recipient_extension": event.recipient_extension, **context, "timestamp": timestamp, } @@ -767,7 +799,7 @@ async def mission_telefoni_ringing(event: MissionCallEvent, request: Request, to @router.get("/mission/webhook/telefoni/ringing") async def mission_telefoni_ringing_get(request: Request, token: Optional[str] = Query(None)): - _validate_mission_webhook_token(request, token) + _validate_mission_webhook_token(request, token, allow_telefoni_token=True) # Allow token-only GET calls (no call payload) for phone webhook validation/ping. if not _first_query_param(request, "call_id", "callid", "id", "session_id", "uuid"): diff --git a/app/modules/telefoni/templates/log.html b/app/modules/telefoni/templates/log.html index 186c33c..7e34967 100644 --- a/app/modules/telefoni/templates/log.html +++ b/app/modules/telefoni/templates/log.html @@ -11,6 +11,18 @@ +
Generér de præcise URL-strenge til telefonernes Action URL felter (Established + Terminated).
+Generér de præcise URL-strenge til telefonernes Action URL felter. Indgående opkald vises live i Mission Control og på Telefoni-siden.
$call_id, $remote, $local, $active_user). Ikke-dokumenterede placeholders som $callid/$caller/$callee bliver ofte ikke erstattet.
?token=... automatisk.