bmc_hub/app/services/sms_service.py
Christian 0831715d3a feat: add SMS service and frontend integration
- Implement SmsService class for sending SMS via CPSMS API.
- Add SMS sending functionality in the frontend with validation and user feedback.
- Create database migrations for SMS message storage and telephony features.
- Introduce telephony settings and user-specific configurations for click-to-call functionality.
- Enhance user experience with toast notifications for incoming calls and actions.
2026-02-14 02:26:29 +01:00

97 lines
3.2 KiB
Python

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")