bmc_hub/app/modules/remote/telephony.py

88 lines
4.5 KiB
Python
Raw Normal View History

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