import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import { PlatformGraphqlService } from './platform/platform-graphql.service'; // On startup, registers this sidecar with the telephony dispatcher // so Ozonetel events are routed to the correct sidecar by agentId. // // Flow: // 1. Load agent list from platform (Agent entities in this workspace) // 2. POST /api/supervisor/register to the dispatcher // 3. Start heartbeat interval (every 30s) // 4. On shutdown, DELETE /api/supervisor/register const HEARTBEAT_INTERVAL_MS = 30_000; @Injectable() export class TelephonyRegistrationService implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(TelephonyRegistrationService.name); private heartbeatTimer: NodeJS.Timeout | null = null; constructor( private config: ConfigService, private platform: PlatformGraphqlService, ) {} private get dispatcherUrl(): string { return this.config.get('TELEPHONY_DISPATCHER_URL') ?? ''; } private get sidecarUrl(): string { return this.config.get('TELEPHONY_CALLBACK_URL') ?? ''; } private get workspace(): string { return process.env.PLATFORM_WORKSPACE_SUBDOMAIN ?? 'unknown'; } async onModuleInit() { if (!this.dispatcherUrl || !this.sidecarUrl) { this.logger.warn('TELEPHONY_DISPATCHER_URL or TELEPHONY_CALLBACK_URL not set — skipping telephony registration'); return; } await this.register(); this.heartbeatTimer = setInterval(async () => { try { await axios.post(`${this.dispatcherUrl}/api/supervisor/heartbeat`, { sidecarUrl: this.sidecarUrl, }, { timeout: 5000 }); } catch (err: any) { this.logger.warn(`Heartbeat failed: ${err.message} — attempting re-registration`); await this.register(); } }, HEARTBEAT_INTERVAL_MS); } async onModuleDestroy() { if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); if (this.dispatcherUrl && this.sidecarUrl) { try { await axios.delete(`${this.dispatcherUrl}/api/supervisor/register`, { data: { sidecarUrl: this.sidecarUrl }, timeout: 5000, }); this.logger.log('Deregistered from telephony dispatcher'); } catch { // Best-effort — TTL will clean up anyway } } } private async register() { try { const agents = await this.loadAgentIds(); if (agents.length === 0) { this.logger.warn('No agents found in workspace — skipping registration'); return; } await axios.post(`${this.dispatcherUrl}/api/supervisor/register`, { sidecarUrl: this.sidecarUrl, workspace: this.workspace, agents, }, { timeout: 5000 }); this.logger.log(`Registered with telephony dispatcher: ${agents.length} agents (${agents.join(', ')})`); } catch (err: any) { this.logger.error(`Registration failed: ${err.message}`); } } private async loadAgentIds(): Promise { try { const apiKey = this.config.get('PLATFORM_API_KEY'); if (!apiKey) return []; const data = await this.platform.queryWithAuth( `{ agents(first: 50) { edges { node { ozonetelAgentId } } } }`, undefined, `Bearer ${apiKey}`, ); return (data.agents?.edges ?? []) .map((e: any) => e.node.ozonetelAgentId) .filter((id: string) => id && id !== 'PENDING'); } catch (err: any) { this.logger.warn(`Failed to load agents from platform: ${err.message}`); return []; } } }