import base64 import json import logging import re from typing import Dict, Any from urllib.error import HTTPError, URLError from urllib.request import Request as UrlRequest, urlopen from app.core.config import settings logger = logging.getLogger(__name__) class SmsService: API_URL = "https://api.cpsms.dk/v2/send" @staticmethod def normalize_recipient(number: str) -> str: cleaned = re.sub(r"[^0-9+]", "", (number or "").strip()) if not cleaned: raise ValueError("Mobilnummer mangler") if cleaned.startswith("+"): cleaned = cleaned[1:] if cleaned.startswith("00"): cleaned = cleaned[2:] if not cleaned.isdigit(): raise ValueError("Ugyldigt mobilnummer") if len(cleaned) == 8: cleaned = "45" + cleaned if len(cleaned) < 8: raise ValueError("Ugyldigt mobilnummer") return cleaned @staticmethod def _authorization_header() -> str: username = (settings.SMS_USERNAME or "").strip() api_key = (settings.SMS_API_KEY or "").strip() if not username or not api_key: raise ValueError("SMS er ikke konfigureret (SMS_USERNAME/SMS_API_KEY mangler)") raw = f"{username}:{api_key}".encode("utf-8") token = base64.b64encode(raw).decode("ascii") return f"Basic {token}" @classmethod def send_sms(cls, to: str, message: str, sender: str | None = None) -> Dict[str, Any]: sms_message = (message or "").strip() if not sms_message: raise ValueError("SMS-besked må ikke være tom") if len(sms_message) > 1530: raise ValueError("SMS-besked er for lang (max 1530 tegn)") recipient = cls.normalize_recipient(to) sms_sender = (sender or settings.SMS_SENDER or "").strip() if not sms_sender: raise ValueError("SMS afsender mangler (SMS_SENDER)") payload = { "to": recipient, "message": sms_message, "from": sms_sender, } body = json.dumps(payload).encode("utf-8") request = UrlRequest(cls.API_URL, data=body, method="POST") request.add_header("Content-Type", "application/json") request.add_header("Authorization", cls._authorization_header()) try: with urlopen(request, timeout=15) as response: response_body = response.read().decode("utf-8") response_data = json.loads(response_body) if response_body else {} return { "http_status": getattr(response, "status", 200), "provider": "cpsms", "recipient": recipient, "result": response_data, } except HTTPError as e: error_body = "" try: error_body = e.read().decode("utf-8") except Exception: error_body = "" logger.error("❌ CPSMS HTTP error: status=%s body=%s", e.code, error_body) raise RuntimeError(f"CPSMS fejl ({e.code})") except URLError as e: logger.error("❌ CPSMS connection error: %s", e) raise RuntimeError("Kunne ikke kontakte CPSMS")