Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/telephony/live_service.py
2026-08-09 02:12:21 +03:30

192 lines
5.6 KiB
Python

"""داشبورد زنده، BLF و کنترل تماس."""
from __future__ import annotations
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session, joinedload
from adapters.db.models.telephony import (
TelephonyCall,
TelephonyCommand,
TelephonyExtension,
TelephonyPbxConnection,
TelephonyQueue,
TelephonyUserExtension,
)
from app.core.responses import ApiError
from app.services.telephony.telephony_service import call_to_dict, extension_to_dict, list_pbx
def _now() -> datetime:
return datetime.utcnow()
def live_snapshot(db: Session, business_id: int) -> Dict[str, Any]:
active = (
db.query(TelephonyCall)
.filter(
TelephonyCall.business_id == business_id,
TelephonyCall.status.in_(["ringing", "answered"]),
TelephonyCall.ended_at.is_(None),
)
.order_by(TelephonyCall.started_at.desc())
.limit(100)
.all()
)
waiting = [c for c in active if c.status == "ringing"]
talking = [c for c in active if c.status == "answered"]
exts = (
db.query(TelephonyExtension)
.filter(TelephonyExtension.business_id == business_id, TelephonyExtension.is_active == True) # noqa: E712
.order_by(TelephonyExtension.extension.asc())
.all()
)
# refresh stale presence to unavailable if PBX offline
pbx_status = {p["id"]: p["status"] for p in list_pbx(db, business_id)}
blf = []
for e in exts:
presence = e.presence_status
if pbx_status.get(e.pbx_id) != "online" and presence not in ("busy", "ringing"):
presence = "unavailable"
# derive from active calls
on_call = next((c for c in active if c.extension == e.extension), None)
if on_call:
presence = "ringing" if on_call.status == "ringing" else "busy"
blf.append({**extension_to_dict(e), "presence_status": presence, "active_call_id": on_call.id if on_call else None})
queues = (
db.query(TelephonyQueue)
.filter(TelephonyQueue.business_id == business_id, TelephonyQueue.is_active == True) # noqa: E712
.all()
)
today = _now().replace(hour=0, minute=0, second=0, microsecond=0)
missed_today = (
db.query(TelephonyCall)
.filter(
TelephonyCall.business_id == business_id,
TelephonyCall.status == "missed",
TelephonyCall.started_at >= today,
)
.count()
)
online_ops = len([b for b in blf if b["presence_status"] in ("idle", "busy", "ringing")])
return {
"generated_at": _now().isoformat(),
"kpis": {
"active": len(active),
"waiting": len(waiting),
"talking": len(talking),
"missed_today": missed_today,
"operators_online": online_ops,
},
"active_calls": [call_to_dict(c) for c in active],
"blf": blf,
"queues": [
{
"id": q.id,
"queue_code": q.queue_code,
"name": q.name,
"waiting": q.live_waiting_count,
"talking": q.live_talking_count,
}
for q in queues
],
"pbx": list_pbx(db, business_id),
}
def update_presence_from_event(
db: Session, business_id: int, extension: Optional[str], status: str
) -> None:
if not extension:
return
rows = (
db.query(TelephonyExtension)
.filter(TelephonyExtension.business_id == business_id, TelephonyExtension.extension == extension)
.all()
)
for row in rows:
row.presence_status = status
row.presence_updated_at = _now()
row.updated_at = _now()
def enqueue_control_command(
db: Session,
business_id: int,
user_id: int,
*,
command_type: str,
call_id: Optional[int] = None,
extension: Optional[str] = None,
target: Optional[str] = None,
pbx_id: Optional[int] = None,
session_id: Optional[str] = None,
) -> Dict[str, Any]:
if command_type not in ("hangup", "transfer", "hold", "resume"):
raise ApiError("VALIDATION_ERROR", "نوع دستور نامعتبر است.", http_status=400)
call = None
if call_id:
call = (
db.query(TelephonyCall)
.filter(TelephonyCall.id == call_id, TelephonyCall.business_id == business_id)
.first()
)
if not call:
raise ApiError("NOT_FOUND", "تماس یافت نشد.", http_status=404)
pbx_id = call.pbx_id
extension = extension or call.extension
if not pbx_id:
# pick primary extension pbx for user
link = (
db.query(TelephonyUserExtension)
.options(joinedload(TelephonyUserExtension.extension_row))
.filter(
TelephonyUserExtension.business_id == business_id,
TelephonyUserExtension.user_id == user_id,
TelephonyUserExtension.is_primary == True, # noqa: E712
)
.first()
)
if not link:
raise ApiError("NO_EXTENSION", "داخلی برای کنترل تماس تعریف نشده.", http_status=400)
pbx_id = link.pbx_id
extension = extension or (link.extension_row.extension if link.extension_row else None)
pbx = (
db.query(TelephonyPbxConnection)
.filter(TelephonyPbxConnection.id == pbx_id, TelephonyPbxConnection.business_id == business_id)
.first()
)
if not pbx or not pbx.is_active:
raise ApiError("PBX_OFFLINE", "مرکز تلفن در دسترس نیست.", http_status=400)
command_id = str(uuid.uuid4())
extra = dict(call.extra_info or {}) if call else {}
payload = {
"extension": extension,
"target": target,
"asterisk_uniqueid": call.asterisk_uniqueid if call else None,
"channel": extra.get("channel"),
"session_id": session_id or extra.get("session_id"),
"audiosocket_uuid": extra.get("audiosocket_uuid"),
}
cmd = TelephonyCommand(
business_id=business_id,
pbx_id=pbx.id,
command_id=command_id,
command_type=command_type,
payload=payload,
status="pending",
created_by_user_id=user_id,
call_id=call.id if call else None,
created_at=_now(),
expires_at=_now() + timedelta(minutes=1),
)
db.add(cmd)
db.commit()
return {"ok": True, "command_id": command_id, "type": command_type, "call_id": call.id if call else None}