88 lines
4.5 KiB
Python
88 lines
4.5 KiB
Python
|
|
"""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
|