From 79f9e5bb24d25743d2414552940f84a2d602a514 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 13 Jul 2025 09:53:02 +0300 Subject: [PATCH] add livekit webhooks --- backend/package.json | 1 + backend/src/core/core.module.ts | 6 ++-- backend/src/main.ts | 2 +- .../module/stream/ingress/ingress.module.ts | 6 ++-- .../src/module/webhook/webhook.controller.ts | 20 +++++++++++++ backend/src/module/webhook/webhook.module.ts | 16 ++++++++++ backend/src/module/webhook/webhook.service.ts | 30 +++++++++++++++++++ .../shared/middlewares/raw-body.middleware.ts | 21 +++++++++++++ 8 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 backend/src/module/webhook/webhook.controller.ts create mode 100644 backend/src/module/webhook/webhook.module.ts create mode 100644 backend/src/module/webhook/webhook.service.ts create mode 100644 backend/src/shared/middlewares/raw-body.middleware.ts diff --git a/backend/package.json b/backend/package.json index 9a3ffea..c1c3058 100644 --- a/backend/package.json +++ b/backend/package.json @@ -55,6 +55,7 @@ "otpauth": "^9.4.0", "prisma": "^6.10.1", "qrcode": "^1.5.4", + "raw-body": "^3.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", "reflect-metadata": "^0.2.2", diff --git a/backend/src/core/core.module.ts b/backend/src/core/core.module.ts index 82c81ea..43c054f 100644 --- a/backend/src/core/core.module.ts +++ b/backend/src/core/core.module.ts @@ -1,5 +1,5 @@ import { getGraphQLConfig } from '@/src/core/config/graphql.config' -import { getLiveKitConfig } from '@/src/core/config/livekit.config'; +import { getLiveKitConfig } from '@/src/core/config/livekit.config' import { AccountModule } from '@/src/module/auth/account/account.module' import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module' import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module' @@ -11,8 +11,9 @@ import { CronModule } from '@/src/module/cron/cron.module' import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module' import { MailModule } from '@/src/module/libs/mail/mail.module' import { StorageModule } from '@/src/module/libs/storage/storage.module' -import { IngressModule } from '@/src/module/stream/ingress/ingress.module'; +import { IngressModule } from '@/src/module/stream/ingress/ingress.module' import { StreamModule } from '@/src/module/stream/stream.module' +import { WebhookModule } from '@/src/module/webhook/webhook.module' import { IS_DEV } from '@/src/shared/util/is-dev.util' import { ApolloDriver } from '@nestjs/apollo' import { Module } from '@nestjs/common' @@ -52,6 +53,7 @@ import { RedisModule } from './redis/redis.module' ProfileModule, StreamModule, IngressModule, + WebhookModule, ], }) export class CoreModule {} diff --git a/backend/src/main.ts b/backend/src/main.ts index 09a55e8..f46b2b4 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -8,7 +8,7 @@ import RedisStore from 'connect-redis' import * as cookieParser from 'cookie-parser' import { CoreModule } from '@/src/core/core.module' import * as session from 'express-session' -import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; +import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js' async function bootstrap() { const app = await NestFactory.create(CoreModule) diff --git a/backend/src/module/stream/ingress/ingress.module.ts b/backend/src/module/stream/ingress/ingress.module.ts index f171ff1..f40c36d 100644 --- a/backend/src/module/stream/ingress/ingress.module.ts +++ b/backend/src/module/stream/ingress/ingress.module.ts @@ -1,6 +1,6 @@ -import { Module } from '@nestjs/common'; -import { IngressService } from './ingress.service'; -import { IngressResolver } from './ingress.resolver'; +import { Module } from '@nestjs/common' +import { IngressService } from './ingress.service' +import { IngressResolver } from './ingress.resolver' @Module({ providers: [IngressResolver, IngressService], diff --git a/backend/src/module/webhook/webhook.controller.ts b/backend/src/module/webhook/webhook.controller.ts new file mode 100644 index 0000000..85b372b --- /dev/null +++ b/backend/src/module/webhook/webhook.controller.ts @@ -0,0 +1,20 @@ +import { Controller, HttpCode, HttpStatus, Post, UnauthorizedException, Headers, Body } from '@nestjs/common' +import { WebhookService } from './webhook.service' + +@Controller('webhook') +export class WebhookController { + constructor(private readonly webhookService: WebhookService) {} + + @Post('livekit') + @HttpCode(HttpStatus.OK) + public receiveWebhookLiveKit( + @Body() body: string, + @Headers('Authorization') authorization: string, + ) { + if (!authorization) { + throw new UnauthorizedException('Отстутсвует авторизация') + } + + return this.webhookService.receiveWebhookLiveKit(body, authorization) + } +} diff --git a/backend/src/module/webhook/webhook.module.ts b/backend/src/module/webhook/webhook.module.ts new file mode 100644 index 0000000..4fa36b5 --- /dev/null +++ b/backend/src/module/webhook/webhook.module.ts @@ -0,0 +1,16 @@ +import { RawBodyMiddleware } from '@/src/shared/middlewares/raw-body.middleware' +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common' +import { WebhookController } from './webhook.controller' +import { WebhookService } from './webhook.service' + +@Module({ + controllers: [WebhookController], + providers: [WebhookService], +}) +export class WebhookModule { + public configure(consumer: MiddlewareConsumer) { + consumer + .apply(RawBodyMiddleware) + .forRoutes({ path: 'webhook/livekit', method: RequestMethod.POST }) + } +} diff --git a/backend/src/module/webhook/webhook.service.ts b/backend/src/module/webhook/webhook.service.ts new file mode 100644 index 0000000..0d11e68 --- /dev/null +++ b/backend/src/module/webhook/webhook.service.ts @@ -0,0 +1,30 @@ +import { PrismaService } from '@/src/core/prisma/prisma.service' +import { LiveKitService } from '@/src/module/libs/livekit/livekit.service' +import { Injectable } from '@nestjs/common' + +@Injectable() +export class WebhookService { + constructor( + private prismaService: PrismaService, + private liveKitService: LiveKitService, + ) { + } + + public async receiveWebhookLiveKit(body: string, authorization: string) { + const event = this.liveKitService.webhook.receive(body, authorization, true) + + if (event.event === 'ingress_started' && event.ingressInfo?.ingressId) { + await this.prismaService.stream.update({ + where: { ingressId: event.ingressInfo.ingressId }, + data: { isLive: true }, + }) + } + + if (event.event === 'ingress_ended' && event.ingressInfo?.ingressId) { + await this.prismaService.stream.update({ + where: { ingressId: event.ingressInfo.ingressId }, + data: { isLive: false }, + }) + } + } +} diff --git a/backend/src/shared/middlewares/raw-body.middleware.ts b/backend/src/shared/middlewares/raw-body.middleware.ts new file mode 100644 index 0000000..041c9c0 --- /dev/null +++ b/backend/src/shared/middlewares/raw-body.middleware.ts @@ -0,0 +1,21 @@ +import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common' +import { NextFunction, Request, Response } from 'express' +import * as getRawBody from 'raw-body' + +@Injectable() +export class RawBodyMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + if (!req.readable) { + return next(new BadRequestException('Неправильный запрос')) + } + + getRawBody(req, { encoding: 'utf-8' }) + .then((rawBody) => { + req.body = rawBody + next() + }) + .catch((error: string) => { + throw new BadRequestException('Ошибка при получении', error) + }) + } +}