WIP - twilio integration
This commit is contained in:
195
backend/src/voice/voice.controller.ts
Normal file
195
backend/src/voice/voice.controller.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Req,
|
||||
Res,
|
||||
UseGuards,
|
||||
Logger,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { VoiceService } from './voice.service';
|
||||
import { VoiceGateway } from './voice.gateway';
|
||||
import { InitiateCallDto } from './dto/initiate-call.dto';
|
||||
import { TenantId } from '../tenant/tenant.decorator';
|
||||
|
||||
@Controller('voice')
|
||||
export class VoiceController {
|
||||
private readonly logger = new Logger(VoiceController.name);
|
||||
|
||||
constructor(
|
||||
private readonly voiceService: VoiceService,
|
||||
private readonly voiceGateway: VoiceGateway,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initiate outbound call via REST
|
||||
*/
|
||||
@Post('call')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async initiateCall(
|
||||
@Body() body: InitiateCallDto,
|
||||
@Req() req: any,
|
||||
@TenantId() tenantId: string,
|
||||
) {
|
||||
const userId = req.user?.userId || req.user?.sub;
|
||||
|
||||
const result = await this.voiceService.initiateCall({
|
||||
tenantId,
|
||||
userId,
|
||||
toNumber: body.toNumber,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get call history
|
||||
*/
|
||||
@Get('calls')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getCallHistory(
|
||||
@Req() req: any,
|
||||
@TenantId() tenantId: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const userId = req.user?.userId || req.user?.sub;
|
||||
const calls = await this.voiceService.getCallHistory(
|
||||
tenantId,
|
||||
userId,
|
||||
limit ? parseInt(limit) : 50,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: calls,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TwiML for outbound calls
|
||||
*/
|
||||
@Post('twiml/outbound')
|
||||
async outboundTwiml(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
|
||||
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Response>
|
||||
<Start>
|
||||
<Stream url="wss://${req.headers.host}/api/voice/stream" />
|
||||
</Start>
|
||||
<Say>Connecting your call</Say>
|
||||
<Dial>
|
||||
<Number>${(req.body as any).To}</Number>
|
||||
</Dial>
|
||||
</Response>`;
|
||||
|
||||
res.type('text/xml').send(twiml);
|
||||
}
|
||||
|
||||
/**
|
||||
* TwiML for inbound calls
|
||||
*/
|
||||
@Post('twiml/inbound')
|
||||
async inboundTwiml(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
|
||||
const body = req.body as any;
|
||||
const callSid = body.CallSid;
|
||||
const fromNumber = body.From;
|
||||
const toNumber = body.To;
|
||||
|
||||
this.logger.log(`Incoming call: ${callSid} from ${fromNumber} to ${toNumber}`);
|
||||
|
||||
// TODO: Determine tenant from phone number mapping
|
||||
// TODO: Find available user to route call to
|
||||
// For now, return a simple TwiML response
|
||||
|
||||
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Response>
|
||||
<Start>
|
||||
<Stream url="wss://${req.headers.host}/api/voice/stream" />
|
||||
</Start>
|
||||
<Say>Please wait while we connect you to an agent</Say>
|
||||
<Dial>
|
||||
<Queue>support</Queue>
|
||||
</Dial>
|
||||
</Response>`;
|
||||
|
||||
res.type('text/xml').send(twiml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Twilio status webhook
|
||||
*/
|
||||
@Post('webhook/status')
|
||||
async statusWebhook(@Req() req: FastifyRequest) {
|
||||
const body = req.body as any;
|
||||
const callSid = body.CallSid;
|
||||
const status = body.CallStatus;
|
||||
const duration = body.CallDuration ? parseInt(body.CallDuration) : undefined;
|
||||
|
||||
this.logger.log(`Call status update: ${callSid} -> ${status}`);
|
||||
|
||||
// TODO: Extract tenant ID from call record
|
||||
// For now, we'll need to lookup the call to get tenant ID
|
||||
// This is a limitation - we should store tenantId in call metadata
|
||||
|
||||
try {
|
||||
// Update call status
|
||||
// await this.voiceService.updateCallStatus({
|
||||
// callSid,
|
||||
// tenantId: 'LOOKUP_NEEDED',
|
||||
// status,
|
||||
// duration,
|
||||
// });
|
||||
|
||||
// Notify user via WebSocket
|
||||
// await this.voiceGateway.notifyCallUpdate(userId, {
|
||||
// callSid,
|
||||
// status,
|
||||
// duration,
|
||||
// });
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to process status webhook', error);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Twilio recording webhook
|
||||
*/
|
||||
@Post('webhook/recording')
|
||||
async recordingWebhook(@Req() req: FastifyRequest) {
|
||||
const body = req.body as any;
|
||||
const callSid = body.CallSid;
|
||||
const recordingUrl = body.RecordingUrl;
|
||||
|
||||
this.logger.log(`Recording available for call ${callSid}: ${recordingUrl}`);
|
||||
|
||||
// TODO: Update call record with recording URL
|
||||
// TODO: Trigger transcription if needed
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket endpoint for Twilio Media Streams
|
||||
*/
|
||||
@Post('stream')
|
||||
async mediaStream(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
|
||||
// Twilio Media Streams use WebSocket protocol
|
||||
// This would need to be handled by the WebSocket server
|
||||
// In Fastify, we need to upgrade the connection
|
||||
|
||||
this.logger.log('Media stream connection requested');
|
||||
|
||||
// TODO: Implement WebSocket upgrade for media streams
|
||||
// This will handle bidirectional audio streaming between Twilio and OpenAI
|
||||
|
||||
res.send({ message: 'WebSocket upgrade required' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user