import { Controller, Post, Get, Body, HttpException, Logger } from '@nestjs/common'; import axios from 'axios'; import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.service'; import { SupervisorService } from './supervisor.service'; import { TelephonyConfigService } from '../config/telephony-config.service'; // Supervisor barge/whisper/listen endpoints. // Proxies requests to Ozonetel's dashboardApi using admin JWT auth. // // API reference (from CA-Admin source code): // apiId 63 → CALL_BARGEIN (initiate barge) // apiId 158 → Redis barge state (insert/delete) // apiId 139 → SIP credential pool (sipSubscribe) @Controller('api/supervisor/barge') export class SupervisorBargeController { private readonly logger = new Logger(SupervisorBargeController.name); private readonly dashboardApiUrl = 'https://api.cloudagent.ozonetel.com/dashboardApi/monitor/api'; private readonly adminApiUrl = 'https://api.cloudagent.ozonetel.com/ca-admin-Api/CloudAgentAPI'; constructor( private readonly adminAuth: OzonetelAdminAuthService, private readonly supervisor: SupervisorService, private readonly telephony: TelephonyConfigService, ) {} @Get('sip-credentials') async getSipCredentials() { if (!this.adminAuth.isConfigured()) { throw new HttpException('Ozonetel admin not configured — add credentials in Settings → Telephony', 503); } const config = this.telephony.getConfig(); const sipGateway = `${config.sip.domain}:${config.sip.wsPort}`; const headers = await this.adminAuth.getAuthHeaders(); try { const res = await axios.post(`${this.adminApiUrl}/endpoint/sipnumber/sipSubscribe`, { apiId: 139, sipURL: sipGateway, }, { headers }); const data = res.data; this.logger.log(`[BARGE] SIP credentials response: ${JSON.stringify(data)}`); if (!data?.sip_number) { throw new HttpException('No SIP numbers available in pool', 503); } return { sipNumber: data.sip_number, sipPassword: data.password, sipDomain: data.pop_location ?? config.sip.domain, sipPort: config.sip.wsPort, }; } catch (err: any) { this.logger.error(`[BARGE] SIP credentials failed: ${err.message}`); if (err instanceof HttpException) throw err; throw new HttpException('Failed to fetch SIP credentials', 502); } } @Post() async initiateBarge(@Body() body: { ucid: string; agentId: string; agentNumber: string; supervisorId?: string }) { if (!body.ucid || !body.agentNumber) { throw new HttpException('ucid and agentNumber required', 400); } if (!this.adminAuth.isConfigured()) { throw new HttpException('Ozonetel admin not configured — add credentials in Settings → Telephony', 503); } // Prevent double-barge on same agent const existing = this.supervisor.getBargeSession(body.agentId); if (existing) { throw new HttpException(`Agent ${body.agentId} is already being monitored`, 409); } // Get SIP credentials from Ozonetel pool const sipCreds = await this.getSipCredentials(); const headers = await this.adminAuth.getAuthHeaders(); try { const res = await axios.post(this.dashboardApiUrl, { apiId: 63, ucid: body.ucid, action: 'CALL_BARGEIN', isSip: true, phoneno: sipCreds.sipNumber, agentNumber: body.agentNumber, cbURL: 'helix-engage', }, { headers }); this.logger.log(`[BARGE] Initiated: ucid=${body.ucid} agent=${body.agentId} sip=${sipCreds.sipNumber} response=${JSON.stringify(res.data)}`); // Track the session this.supervisor.startBargeSession({ supervisorId: body.supervisorId ?? 'admin', agentId: body.agentId, sipNumber: sipCreds.sipNumber, mode: 'listen', startedAt: new Date().toISOString(), }); return { status: 'ok', ...sipCreds, ozonetelResponse: res.data, }; } catch (err: any) { this.logger.error(`[BARGE] Initiation failed: ${err.message} ${err.response?.data ? JSON.stringify(err.response.data) : ''}`); throw new HttpException(`Barge failed: ${err.response?.data?.Message ?? err.message}`, 502); } } @Post('mode') async updateMode(@Body() body: { agentId: string; mode: 'listen' | 'whisper' | 'barge' }) { if (!body.agentId || !body.mode) { throw new HttpException('agentId and mode required', 400); } if (!['listen', 'whisper', 'barge'].includes(body.mode)) { throw new HttpException('mode must be listen, whisper, or barge', 400); } const session = this.supervisor.getBargeSession(body.agentId); if (!session) { throw new HttpException('No active barge session for this agent', 404); } this.supervisor.updateBargeMode(body.agentId, body.mode); return { status: 'ok', mode: body.mode }; } @Post('end') async endBarge(@Body() body: { agentId: string }) { if (!body.agentId) { throw new HttpException('agentId required', 400); } const session = this.supervisor.getBargeSession(body.agentId); if (!session) { return { status: 'ok', message: 'No active session' }; } // Clear Redis tracking on Ozonetel side (best-effort) if (this.adminAuth.isConfigured()) { try { const headers = await this.adminAuth.getAuthHeaders(); await axios.post(this.dashboardApiUrl, { apiId: 158, Action: 'delete', AgentId: body.agentId, Sip: session.sipNumber, }, { headers }); this.logger.log(`[BARGE] Redis cleanup: ${body.agentId}`); } catch (err: any) { this.logger.warn(`[BARGE] Redis cleanup failed (non-critical): ${err.message}`); } } this.supervisor.endBargeSession(body.agentId); return { status: 'ok' }; } }