diff --git a/src/CollaborationSession/collaborationsession.module.ts b/src/CollaborationSession/collaborationsession.module.ts index c674c12..2119d6e 100644 --- a/src/CollaborationSession/collaborationsession.module.ts +++ b/src/CollaborationSession/collaborationsession.module.ts @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common' import { CollaborationSessionSchema } from './collaborationsession.schema' import { LiveKitModule } from 'src/LiveKit/livekit.module' import { LiveKitService } from 'src/LiveKit/livekit.service' +import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service' @Module({ imports: [LiveKitModule], - providers: [CollaborationSessionSchema, LiveKitService], + providers: [CollaborationSessionSchema, LiveKitService, LiveKitRoomService], exports: [CollaborationSessionSchema], }) export class CollaborationSessionModule {} diff --git a/src/CollaborationSession/collaborationsession.schema.ts b/src/CollaborationSession/collaborationsession.schema.ts index 9a32b14..49729ea 100644 --- a/src/CollaborationSession/collaborationsession.schema.ts +++ b/src/CollaborationSession/collaborationsession.schema.ts @@ -6,12 +6,14 @@ import { Builder, SchemaContext } from 'src/Graphql/graphql.builder' import { PrismaService } from 'src/Prisma/prisma.service' import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { LiveKitService } from 'src/LiveKit/livekit.service' +import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service' @Injectable() export class CollaborationSessionSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, private readonly liveKitService: LiveKitService, + private readonly liveKitRoomService: LiveKitRoomService, ) { super() } @@ -101,11 +103,7 @@ export class CollaborationSessionSchema extends PothosSchema { // } // check if user is participant - if ( - !collaborationSession.collaboratorsIds.includes(ctx.http.me.id) || - ctx.http.me.id === 'user_2nkDilSYEiljIraFGF9PENjILPr' - ) - throw new Error('User not allowed') + if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id)) throw new Error('User not allowed') return collaborationSession } /* ---------- use case 2 : center mentor get collaboration session by schedule date id --------- */ @@ -164,6 +162,14 @@ export class CollaborationSessionSchema extends PothosSchema { }, }) } + // create meeting room + const meetingRoom = await this.prisma.meetingRoom.create({ + data: { + collaborationSessionId: newCollaborationSession.id, + }, + }) + // create livekit room + await this.liveKitRoomService.createServiceMeetingRoom(meetingRoom.id) return newCollaborationSession // if not exist use case } return collaborationSession // if exist use case diff --git a/src/Document/document.schema.ts b/src/Document/document.schema.ts index fb65f08..6b311f5 100644 --- a/src/Document/document.schema.ts +++ b/src/Document/document.schema.ts @@ -352,6 +352,28 @@ export class DocumentSchema extends PothosSchema { }) }, }), + editCollaboratorPermission: t.prismaField({ + type: this.documentCollaborator(), + args: { + documentId: t.arg({ type: 'String', required: true }), + userId: t.arg({ type: 'String', required: true }), + readable: t.arg({ type: 'Boolean', required: true }), + writable: t.arg({ type: 'Boolean', required: true }), + }, + resolve: async (_, __, args, ctx: SchemaContext) => { + if (ctx.isSubscription) throw new Error('Not allowed') + // check if ctx user is owner of document + const document = await this.prisma.document.findUnique({ + where: { id: args.documentId }, + }) + if (!document) throw new Error('Document not found') + if (document.ownerId !== ctx.http?.me?.id) throw new Error('User is not owner of document') + return await this.prisma.documentCollaborator.update({ + where: { documentId_userId: { documentId: args.documentId, userId: args.userId } }, + data: { readable: args.readable, writable: args.writable }, + }) + }, + }), })) this.builder.subscriptionFields((t) => ({ diff --git a/src/LiveKit/livekit.egress.ts b/src/LiveKit/livekit.egress.ts new file mode 100644 index 0000000..b8cb2b7 --- /dev/null +++ b/src/LiveKit/livekit.egress.ts @@ -0,0 +1,40 @@ +// @ts-nocheck +import { Injectable } from '@nestjs/common' +import { + EgressClient, + EncodedFileOutput, + StreamOutput, + EncodedFileType, + EncodingOptionsPreset, +} from 'livekit-server-sdk' + +@Injectable() +export class LiveKitEgressService { + private readonly egressClient = new EgressClient( + process.env.LIVEKIT_URL as string, + process.env.LIVEKIT_API_KEY as string, + process.env.LIVEKIT_API_SECRET as string, + ) + private readonly output = new EncodedFileOutput({ + fileType: EncodedFileType.MP4, + output: { + case: 's3', + value: { + bucket: process.env.RECORDING_BUCKET_NAME as string, + accessKey: process.env.MINIO_ACCESS_KEY as string, + secret: process.env.MINIO_SECRET_KEY as string, + }, + }, + }) + constructor() {} + + async startRecording(roomId: string) { + const egress = await this.egressClient.startRoomCompositeEgress(roomId, this.output, { + layout: 'grid', + encodingOptions: EncodingOptionsPreset.H264_1080P_60, + audioOnly: false, + videoOnly: false, + }) + return egress + } +} diff --git a/src/LiveKit/livekit.module.ts b/src/LiveKit/livekit.module.ts index a55fc91..455345a 100644 --- a/src/LiveKit/livekit.module.ts +++ b/src/LiveKit/livekit.module.ts @@ -1,8 +1,10 @@ import { Module, Global } from '@nestjs/common' import { LiveKitService } from './livekit.service' +import { LiveKitRoomService } from './livekit.room.service' +import { LiveKitEgressService } from './livekit.egress' @Global() @Module({ - providers: [LiveKitService], - exports: [LiveKitService], + providers: [LiveKitService, LiveKitRoomService, LiveKitEgressService], + exports: [LiveKitService, LiveKitRoomService, LiveKitEgressService], }) export class LiveKitModule {} diff --git a/src/LiveKit/livekit.room.service.ts b/src/LiveKit/livekit.room.service.ts index 6a4ab38..348ffde 100644 --- a/src/LiveKit/livekit.room.service.ts +++ b/src/LiveKit/livekit.room.service.ts @@ -1,16 +1,24 @@ -import { forwardRef, Injectable } from '@nestjs/common' +import { Injectable } from '@nestjs/common' // @ts-expect-error import { Room, RoomServiceClient } from 'livekit-server-sdk' +import { LiveKitEgressService } from './livekit.egress' @Injectable() export class LiveKitRoomService { - constructor(private readonly roomServiceClient: RoomServiceClient) {} + private readonly roomServiceClient = new RoomServiceClient( + process.env.LIVEKIT_URL as string, + process.env.LIVEKIT_API_KEY as string, + process.env.LIVEKIT_API_SECRET as string, + ) + constructor(private readonly egressService: LiveKitEgressService) {} async createServiceMeetingRoom(roomId: string) { const room = await this.roomServiceClient.createRoom({ name: roomId, maxParticipants: 2, }) + // start recording + await this.egressService.startRecording(roomId) return room } @@ -22,5 +30,3 @@ export class LiveKitRoomService { return room } } - -export const roomServiceClient = forwardRef(() => LiveKitRoomService) diff --git a/src/LiveKit/livekit.service.ts b/src/LiveKit/livekit.service.ts index 32525e4..7ab5cb9 100644 --- a/src/LiveKit/livekit.service.ts +++ b/src/LiveKit/livekit.service.ts @@ -15,7 +15,9 @@ export class LiveKitService { throw new Error('User must have a name') } const token = new AccessToken(process.env.LIVEKIT_API_KEY as string, process.env.LIVEKIT_API_SECRET as string, { - identity: me.name, + identity: me.id, + name: me.name, + metadata: me.avatarUrl ?? '', }) token.addGrant({ roomJoin: true, @@ -23,4 +25,8 @@ export class LiveKitService { }) return await token.toJwt() } + + getServerUrl() { + return process.env.LIVEKIT_URL + } } diff --git a/src/MeetingRoom/meetingroom.schema.ts b/src/MeetingRoom/meetingroom.schema.ts index c487a98..8d2ed5d 100644 --- a/src/MeetingRoom/meetingroom.schema.ts +++ b/src/MeetingRoom/meetingroom.schema.ts @@ -2,11 +2,13 @@ import { Inject, Injectable } from '@nestjs/common' import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { Builder, SchemaContext } from 'src/Graphql/graphql.builder' import { PrismaService } from 'src/Prisma/prisma.service' +import { LiveKitService } from 'src/LiveKit/livekit.service' @Injectable() export class MeetingRoomSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, + private readonly livekitService: LiveKitService, ) { super() } @@ -24,6 +26,23 @@ export class MeetingRoomSchema extends PothosSchema { }) } + @PothosRef() + meetingRoomJoinInfo() { + return this.builder.simpleObject('MeetingRoomJoinInfo', { + fields: (t) => ({ + id: t.string({ + description: 'The ID of the meeting room.', + }), + token: t.string({ + description: 'The token to join the meeting room.', + }), + serverUrl: t.string({ + description: 'The URL of the server.', + }), + }), + }) + } + @PothosRef() meetingRoomCollaborator() { return this.builder.prismaObject('MeetingRoomCollaborator', { @@ -69,13 +88,50 @@ export class MeetingRoomSchema extends PothosSchema { }) }, }), + // get meeting room info by room id and check if user is collaborator of collaboration session then create new token and return it, + // if not collaborator then throw error + meetingRoomJoinInfo: t.field({ + type: this.meetingRoomJoinInfo(), + args: { + collaborationSessionId: t.arg.string({ + required: true, + }), + }, + resolve: async (_, args, ctx: SchemaContext) => { + if (ctx.isSubscription) throw new Error('Not allowed') + if (!ctx.http.me) throw new Error('Unauthorized') + const meetingRoom = await this.prisma.meetingRoom.findUnique({ + where: { collaborationSessionId: args.collaborationSessionId }, + }) + if (!meetingRoom) throw new Error('Meeting room not found') + // check if user is collaborator of collaboration session + const collaborationSession = await this.prisma.collaborationSession.findUnique({ + where: { id: meetingRoom.collaborationSessionId }, + }) + if (!collaborationSession) throw new Error('Collaboration session not found') + if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id)) + throw new Error('User is not collaborator') + // create new token + const token = await this.livekitService.createToken(ctx.http.me, meetingRoom.id) + return { + id: meetingRoom.id, + token, + serverUrl: this.livekitService.getServerUrl(), + } + }, + }), })) this.builder.mutationFields((t) => ({ createMeetingRoom: t.prismaField({ type: this.meetingRoom(), args: { input: t.arg({ - type: this.builder.generator.getCreateInput('MeetingRoom', ['id', 'createdAt', 'updatedAt']), + type: this.builder.generator.getCreateInput('MeetingRoom', [ + 'id', + 'createdAt', + 'updatedAt', + 'collaborators', + ]), required: true, }), },