"""Contact lookup and restricted control of the owner's existing VoIP phone.""" from fastapi import HTTPException from psycopg2.extras import Json from app.core.database import atomic_transaction, execute_query, execute_query_single from app.dashboard.backend.mission_service import MissionService from .security import actor, remote_device def permitted(user): return bool(user.get('is_superadmin') or 'customers.view' in (user.get('permissions') or [])) def require_access(user): if not permitted(user): raise HTTPException(403, 'Du har ikke adgang til kontakter og telefoni') async def contacts(user, q, offset): require_access(user) from app.contacts.backend.router_simple import get_contacts data = await get_contacts(search=q, customer_id=None, is_active=True, limit=50, offset=offset) fields = ('id', 'first_name', 'last_name', 'email', 'phone', 'mobile', 'company_names') return {'items': [{key: row.get(key) for key in fields} for row in data['contacts']], 'has_more': offset + len(data['contacts']) < data['total']} def status(user): if not permitted(user): return {'allowed': False, 'incoming': []} # Match Mission Control's ring window without changing shared call state. rows = MissionService.get_active_calls() from datetime import datetime, timezone from zoneinfo import ZoneInfo now = datetime.now(timezone.utc) incoming = [] for row in rows: updated = row.get('updated_at') or row.get('started_at') if not updated: continue if updated.tzinfo is None: updated = updated.replace(tzinfo=ZoneInfo('Europe/Copenhagen')) if (now - updated).total_seconds() > MissionService.get_ring_timeout_seconds(): continue incoming.append({key: row.get(key) for key in ('call_id', 'caller_number', 'contact_name', 'company_name', 'ringing_phones')}) return {'allowed': True, 'incoming': incoming} async def call(payload, request): """Persist a reservation before external I/O; never automatically redial on uncertainty.""" from app.modules.telefoni.backend.router import click_to_call from app.modules.telefoni.backend.schemas import TelefoniClickToCallRequest user = actor(remote_device(request)) require_access(user) uid = user['id'] value = {'action': 'call', **payload.model_dump(mode='json')} with atomic_transaction(): execute_query('SELECT user_id FROM users WHERE user_id=%s FOR UPDATE', (uid,)) user = actor(remote_device(request)); require_access(user) old = execute_query_single('SELECT payload,result FROM remote_commands WHERE user_id=%s AND request_id=%s', (uid, str(payload.request_id))) if old: if old['payload'] != value: raise HTTPException(409, 'Handlingen har allerede andre oplysninger') return old['result'] contact = execute_query_single('SELECT id,phone,mobile FROM contacts WHERE id=%s AND is_active=TRUE', (payload.contact_id,)) if not contact or not contact.get(payload.field): raise HTTPException(404, 'Kontakten har ikke det valgte telefonnummer') # Only stored phone numbers can reach the existing action URL template. import re number = str(contact[payload.field]).strip() if not re.fullmatch(r'\+?[0-9][0-9 () .-]{1,39}', number): raise HTTPException(400, 'Kontaktens telefonnummer skal rettes i Hub') pending = {'status': 'uncertain', 'message': 'Opkald er afsendt eller under behandling. Kontrollér telefonen før et nyt opkald.'} execute_query('INSERT INTO remote_commands(user_id,request_id,payload,result) VALUES(%s,%s,%s,%s)', (uid, str(payload.request_id), Json(value), Json(pending))) request.state.user_id = uid try: await click_to_call(TelefoniClickToCallRequest(number=number, user_id=uid), request) result = {'status': 'ok', 'message': 'Opkald startet på din VoIP-telefon'} except HTTPException as exc: # A timeout can mean that the phone received the command. Never redial it. result = {'status': 'failed' if exc.status_code < 500 else 'uncertain', 'message': 'Opkald kunne ikke startes. Kontrollér telefonopsætningen i Hub.' if exc.status_code < 500 else pending['message']} except Exception: result = pending execute_query('UPDATE remote_commands SET result=%s WHERE user_id=%s AND request_id=%s', (Json(result), uid, str(payload.request_id))) return result