Drop required from_number for webhook logging, persist CalleeExtension as lineExtension, and add direction/status/line filters on the calls list. Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
Headers,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
Req,
|
|
UnauthorizedException,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import type { Request } from 'express';
|
|
import { JwtAuthGuard } from '../../auth/jwt-auth.guard';
|
|
import { CurrentUser } from '../../auth/current-user.decorator';
|
|
import { Roles } from '../../auth/roles.decorator';
|
|
import { RolesGuard } from '../../auth/roles.guard';
|
|
import { OrgModuleGuard } from '../../billing/org-module.guard';
|
|
import { RequireOrgModule } from '../../billing/org-module.decorator';
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
import type { VoipWebhookEvent } from '@mento/shared';
|
|
import { VoipConnectionsService } from './connections.service';
|
|
import { VoipCallsService } from './calls.service';
|
|
import {
|
|
CreateVoipConnectionDto,
|
|
UpdateVoipConnectionDto,
|
|
VoipClickToCallDto,
|
|
} from './dto';
|
|
import {
|
|
isDaftaresomaWebhookPayload,
|
|
mapDaftaresomaWebhook,
|
|
} from './daftareshoma-webhook';
|
|
|
|
@Controller('integrations/voip')
|
|
@UseGuards(JwtAuthGuard, RolesGuard, OrgModuleGuard)
|
|
@RequireOrgModule('voip')
|
|
export class VoipController {
|
|
constructor(
|
|
private readonly connections: VoipConnectionsService,
|
|
private readonly calls: VoipCallsService,
|
|
) {}
|
|
|
|
@Get('connections')
|
|
@Roles('owner', 'admin')
|
|
list(@CurrentUser() user: { organizationId: string }) {
|
|
return this.connections.list(user.organizationId);
|
|
}
|
|
|
|
@Post('connections')
|
|
@Roles('owner', 'admin')
|
|
create(
|
|
@CurrentUser() user: { organizationId: string },
|
|
@Body() dto: CreateVoipConnectionDto,
|
|
) {
|
|
return this.connections.create(user.organizationId, dto);
|
|
}
|
|
|
|
@Patch('connections/:id')
|
|
@Roles('owner', 'admin')
|
|
update(
|
|
@CurrentUser() user: { organizationId: string },
|
|
@Param('id') id: string,
|
|
@Body() dto: UpdateVoipConnectionDto,
|
|
) {
|
|
return this.connections.update(user.organizationId, id, dto);
|
|
}
|
|
|
|
@Delete('connections/:id')
|
|
@Roles('owner', 'admin')
|
|
async remove(
|
|
@CurrentUser() user: { organizationId: string },
|
|
@Param('id') id: string,
|
|
) {
|
|
await this.connections.remove(user.organizationId, id);
|
|
return { ok: true };
|
|
}
|
|
|
|
@Get('calls')
|
|
@Roles('owner', 'admin')
|
|
recent(
|
|
@CurrentUser() user: { organizationId: string },
|
|
@Query('connectionId') connectionId?: string,
|
|
@Query('direction') direction?: string,
|
|
@Query('status') status?: string,
|
|
@Query('lineExtension') lineExtension?: string,
|
|
) {
|
|
return this.calls.listRecent(user.organizationId, {
|
|
connectionId,
|
|
direction,
|
|
status,
|
|
lineExtension,
|
|
});
|
|
}
|
|
|
|
@Post('click-to-call')
|
|
clickToCall(
|
|
@CurrentUser() user: { organizationId: string; userId: string },
|
|
@Body() dto: VoipClickToCallDto,
|
|
) {
|
|
return this.calls.clickToCall(
|
|
user.organizationId,
|
|
user.userId,
|
|
dto.number,
|
|
dto.personId,
|
|
);
|
|
}
|
|
}
|
|
|
|
@Controller('public/integrations/voip')
|
|
export class VoipWebhookController {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly connections: VoipConnectionsService,
|
|
private readonly calls: VoipCallsService,
|
|
) {}
|
|
|
|
@Post(':connectionId/webhook')
|
|
async webhook(
|
|
@Param('connectionId') connectionId: string,
|
|
@Headers('x-voip-signature') signature: string | undefined,
|
|
@Headers('x-montact-signature') montactSignature: string | undefined,
|
|
@Req() req: Request & { rawBody?: Buffer },
|
|
@Body() body: VoipWebhookEvent,
|
|
) {
|
|
const row = await this.prisma.voipConnection.findFirst({
|
|
where: { id: connectionId, enabled: true },
|
|
});
|
|
if (!row) throw new UnauthorizedException('اتصال نامعتبر است');
|
|
|
|
const secret = await this.connections.decryptWebhookSecret(row);
|
|
if (secret) {
|
|
const raw =
|
|
req.rawBody ?? Buffer.from(JSON.stringify(body ?? {}), 'utf8');
|
|
const ok = this.connections.verifySignature(
|
|
raw,
|
|
signature ?? montactSignature,
|
|
secret,
|
|
);
|
|
if (!ok) throw new UnauthorizedException('امضای وبهوک نامعتبر است');
|
|
}
|
|
|
|
let event: VoipWebhookEvent | null = null;
|
|
if (
|
|
row.provider === 'daftareshoma' ||
|
|
isDaftaresomaWebhookPayload(body)
|
|
) {
|
|
event = mapDaftaresomaWebhook(body);
|
|
} else if (body?.callId) {
|
|
event = body;
|
|
}
|
|
|
|
if (!event?.callId) {
|
|
return { ok: false, error: 'callId required' };
|
|
}
|
|
|
|
const result = await this.calls.ingestWebhook(connectionId, event);
|
|
return { ok: true, ...result };
|
|
}
|
|
}
|