diff --git a/src/CollaborationSession/collaborationsession.module.ts b/src/CollaborationSession/collaborationsession.module.ts index ca7d09d..fe7fe0d 100644 --- a/src/CollaborationSession/collaborationsession.module.ts +++ b/src/CollaborationSession/collaborationsession.module.ts @@ -1,8 +1,12 @@ 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 { LiveKitParticipantService } from 'src/LiveKit/livekit.participant.service' +import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service' @Module({ - providers: [CollaborationSessionSchema], + imports: [LiveKitModule], + providers: [CollaborationSessionSchema, LiveKitService, LiveKitParticipantService, LiveKitRoomService], exports: [CollaborationSessionSchema], }) export class CollaborationSessionModule {} diff --git a/src/CollaborationSession/collaborationsession.schema.ts b/src/CollaborationSession/collaborationsession.schema.ts index d93928a..c2f2f96 100644 --- a/src/CollaborationSession/collaborationsession.schema.ts +++ b/src/CollaborationSession/collaborationsession.schema.ts @@ -1,17 +1,20 @@ import { Inject, Injectable, Logger } from '@nestjs/common' import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' -import { Builder } from '../Graphql/graphql.builder' +import { Builder, SchemaContext } from '../Graphql/graphql.builder' import { PrismaService } from '../Prisma/prisma.service' // import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service' import { v4 as uuidv4 } from 'uuid' import { CollaborationSession, Role, ScheduleDateStatus } from '@prisma/client' import { DateTimeUtils } from 'src/common/utils/datetime.utils' +import { LiveKitService } from 'src/LiveKit/livekit.service' +import { LiveKitParticipantService } from 'src/LiveKit/livekit.participant.service' @Injectable() export class CollaborationSessionSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, - // private readonly liveKitRoomService: LiveKitRoomService, + private readonly liveKitService: LiveKitService, + private readonly liveKitParticipantService: LiveKitParticipantService, ) { super() } @@ -99,6 +102,7 @@ export class CollaborationSessionSchema extends PothosSchema { // }, // }) // } + // check if user is participant if ( !collaborationSession.collaboratorsIds.includes(ctx.http.me.id) || @@ -182,6 +186,29 @@ export class CollaborationSessionSchema extends PothosSchema { }) }, }), + liveKitToken: t.field({ + type: 'String', + resolve: async (_, _args, ctx: SchemaContext) => { + if (ctx.isSubscription) throw new Error('Not allowed') + if (!ctx.http?.me?.id) throw new Error('User not found') + // check if participantId is in meetingRoomCollaborators + const meetingRoomCollaborator = await this.prisma.meetingRoomCollaborator.findFirst({ + where: { + userId: ctx.http.me.id, + }, + }) + if (!meetingRoomCollaborator) throw new Error('Meeting room collaborator not found') + const meetingRoom = await this.prisma.meetingRoom.findUnique({ + where: { + id: meetingRoomCollaborator.meetingRoomId, + }, + }) + if (!meetingRoom) throw new Error('Meeting room not found') + const token = await this.liveKitService.createAccessToken(ctx.http.me.id) + await this.liveKitParticipantService.grantRoomJoinPermission(token, meetingRoom.collaborationSessionId) + return await this.liveKitParticipantService.toJWT(token) + }, + }), })) this.builder.mutationFields((t) => ({ diff --git a/src/Cron/cron.service.ts b/src/Cron/cron.service.ts index f833c57..37d4d02 100644 --- a/src/Cron/cron.service.ts +++ b/src/Cron/cron.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common' import { Cron } from '@nestjs/schedule' import { CronExpression } from '@nestjs/schedule' -import { OrderStatus, PaymentStatus, ScheduleDateStatus, ScheduleStatus } from '@prisma/client' +import { OrderStatus, PaymentStatus, ScheduleDateStatus, ScheduleStatus, ServiceStatus } from '@prisma/client' import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { NotificationService } from 'src/Notification/notification.service' import { PrismaService } from 'src/Prisma/prisma.service' @@ -189,4 +189,34 @@ export class CronService { } } } + // cron every day to disable service without any schedule in the past 30 days + @Cron(CronExpression.EVERY_DAY_AT_1AM) + async taskDisableServiceWithoutSchedule() { + Logger.log('Disabling service without any schedule', 'taskDisableServiceWithoutSchedule') + const services = await this.prisma.managedService.findMany({ + where: { + NOT: { + schedule: { + some: { + scheduleStart: { gte: DateTimeUtils.now().minus({ days: 30 }).toJSDate() }, + }, + }, + }, + }, + }) + + for (const service of services) { + await this.prisma.managedService.update({ + where: { id: service.id }, + data: { + service: { + update: { + status: ServiceStatus.INACTIVE, + }, + }, + }, + }) + Logger.log(`Service ${service.id} has been disabled`, 'taskDisableServiceWithoutSchedule') + } + } } diff --git a/src/Graphql/graphql.generator.ts b/src/Graphql/graphql.generator.ts index 89d00b6..205f93f 100644 --- a/src/Graphql/graphql.generator.ts +++ b/src/Graphql/graphql.generator.ts @@ -1,6 +1,13 @@ import { Inject, Injectable } from '@nestjs/common' -import { type BaseEnum, type EnumRef, InputObjectRef, type InputType, type InputTypeParam, type SchemaTypes } from '@pothos/core' +import { + type BaseEnum, + type EnumRef, + InputObjectRef, + type InputType, + type InputTypeParam, + type SchemaTypes, +} from '@pothos/core' import { type PrismaModelTypes, getModel } from '@pothos/plugin-prisma' import type { FilterOps } from '@pothos/plugin-prisma-utils' import * as Prisma from '@prisma/client' @@ -76,18 +83,27 @@ export class PrismaCrudGenerator { const withoutFields = model.fields.filter((field) => without?.includes(field.name)) model.fields - .filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name))) + .filter( + (field) => + !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)), + ) .forEach((field) => { let type switch (field.kind) { case 'scalar': - type = field.isList ? this.getScalarListFilter(this.mapScalarType(field.type) as InputType) : this.getFilter(this.mapScalarType(field.type) as InputType) + type = field.isList + ? this.getScalarListFilter(this.mapScalarType(field.type) as InputType) + : this.getFilter(this.mapScalarType(field.type) as InputType) break case 'enum': - type = field.isList ? this.getScalarListFilter(this.getEnum(field.type)) : this.getFilter(this.getEnum(field.type)) + type = field.isList + ? this.getScalarListFilter(this.getEnum(field.type)) + : this.getFilter(this.getEnum(field.type)) break case 'object': - type = field.isList ? this.getListFilter(this.getWhere(field.type as Name)) : this.getWhere(field.type as Name) + type = field.isList + ? this.getListFilter(this.getWhere(field.type as Name)) + : this.getWhere(field.type as Name) break case 'unsupported': break @@ -117,7 +133,13 @@ export class PrismaCrudGenerator { const fields: Record> = {} model.fields - .filter((field) => field.isUnique || field.isId || model.uniqueIndexes.some((index) => index.fields.includes(field.name)) || model.primaryKey?.fields.includes(field.name)) + .filter( + (field) => + field.isUnique || + field.isId || + model.uniqueIndexes.some((index) => index.fields.includes(field.name)) || + model.primaryKey?.fields.includes(field.name), + ) .forEach((field) => { let type switch (field.kind) { @@ -197,7 +219,11 @@ export class PrismaCrudGenerator { const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? []) model.fields - .filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && !relationIds.includes(field.name)) + .filter( + (field) => + !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && + !relationIds.includes(field.name), + ) .forEach((field) => { let type switch (field.kind) { @@ -227,17 +253,22 @@ export class PrismaCrudGenerator { }) } - getCreateRelationInput( - modelName: Name, - relation: Relation, - ) { + getCreateRelationInput< + Name extends string & keyof Types['PrismaTypes'], + Relation extends Model['RelationName'], + Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes + ? Types['PrismaTypes'][Name] + : never, + >(modelName: Name, relation: Relation) { return this.getRef(`${modelName}${capitalize(relation)}`, 'CreateRelationInput', () => { const model = getModel(modelName, this.builder) return this.builder.prismaCreateRelation(modelName, relation, { fields: () => { const relationField = model.fields.find((field) => field.name === relation)! const relatedModel = getModel(relationField.type, this.builder) - const relatedFieldName = relatedModel.fields.find((field) => field.relationName === relationField.relationName)! + const relatedFieldName = relatedModel.fields.find( + (field) => field.relationName === relationField.relationName, + )! return { create: this.getCreateInput(relationField.type as Name, [relatedFieldName.name]), @@ -263,7 +294,11 @@ export class PrismaCrudGenerator { const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? []) model.fields - .filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && !relationIds.includes(field.name)) + .filter( + (field) => + !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && + !relationIds.includes(field.name), + ) .forEach((field) => { let type switch (field.kind) { @@ -304,7 +339,11 @@ export class PrismaCrudGenerator { const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? []) model.fields - .filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && !relationIds.includes(field.name)) + .filter( + (field) => + !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && + !relationIds.includes(field.name), + ) .forEach((field) => { let type switch (field.kind) { @@ -333,17 +372,22 @@ export class PrismaCrudGenerator { }) as InputObjectRef }) } - getUpdateRelationInput( - modelName: Name, - relation: Relation, - ) { + getUpdateRelationInput< + Name extends string & keyof Types['PrismaTypes'], + Relation extends Model['RelationName'], + Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes + ? Types['PrismaTypes'][Name] + : never, + >(modelName: Name, relation: Relation) { return this.getRef(`${modelName}${capitalize(relation)}`, 'UpdateRelationInput', () => { const model = getModel(modelName, this.builder) return this.builder.prismaUpdateRelation(modelName, relation, { fields: () => { const relationField = model.fields.find((field) => field.name === relation)! const relatedModel = getModel(relationField.type, this.builder) - const relatedFieldName = relatedModel.fields.find((field) => field.relationName === relationField.relationName)!.name + const relatedFieldName = relatedModel.fields.find( + (field) => field.relationName === relationField.relationName, + )!.name if (relationField.isList) { return { @@ -446,7 +490,11 @@ export class PrismaCrudGenerator { } } - private getRef>(key: InputType | string, name: string, create: () => T): T { + private getRef>( + key: InputType | string, + name: string, + create: () => T, + ): T { if (!this.refCache.has(key)) { this.refCache.set(key, new Map()) } diff --git a/src/LiveKit/livekit.module.ts b/src/LiveKit/livekit.module.ts index a6437e2..5416158 100644 --- a/src/LiveKit/livekit.module.ts +++ b/src/LiveKit/livekit.module.ts @@ -1,7 +1,10 @@ -// import { Module } from '@nestjs/common' -// import { LiveKitService } from './livekit.service' -// @Module({ -// providers: [LiveKitService], -// exports: [LiveKitService], -// }) -// export class LiveKitModule {} +import { Module } from '@nestjs/common' +import { LiveKitService } from './livekit.service' +import { LiveKitParticipantService } from './livekit.participant.service' +import { LiveKitRoomService } from './livekit.room.service' + +@Module({ + providers: [LiveKitService, LiveKitParticipantService, LiveKitRoomService], + exports: [LiveKitService], +}) +export class LiveKitModule {} diff --git a/src/LiveKit/livekit.participant.service.ts b/src/LiveKit/livekit.participant.service.ts index 64029e1..d77920b 100644 --- a/src/LiveKit/livekit.participant.service.ts +++ b/src/LiveKit/livekit.participant.service.ts @@ -5,13 +5,9 @@ import { AccessToken } from 'livekit-server-sdk' @Injectable() export class LiveKitParticipantService { async createAccessToken(participantId: string) { - return new AccessToken( - process.env.LIVEKIT_API_KEY, - process.env.LIVEKIT_API_SECRET, - { - identity: participantId, - }, - ) + return new AccessToken(process.env.LIVEKIT_API_KEY as string, process.env.LIVEKIT_API_SECRET as string, { + identity: participantId, + }) } async grantRoomJoinPermission(token: AccessToken, roomName: string) { diff --git a/src/LiveKit/livekit.room.service.ts b/src/LiveKit/livekit.room.service.ts index 634a904..022aaae 100644 --- a/src/LiveKit/livekit.room.service.ts +++ b/src/LiveKit/livekit.room.service.ts @@ -1,36 +1,35 @@ -// import { -// Room, -// RoomServiceClient, -// RoomCompositeOptions, -// // @ts-ignore -// } from 'livekit-server-sdk' -// import { v4 as uuidv4 } from 'uuid' +import { v4 as uuidv4 } from 'uuid' -// export class LiveKitRoomService { -// private roomServiceClient: RoomServiceClient +export class LiveKitRoomService { + // biome-ignore lint/suspicious/noExplicitAny: + private roomServiceClient: any + constructor() { + this.initializeRoomServiceClient() + } -// constructor() { -// this.roomServiceClient = new RoomServiceClient( -// process.env.LIVEKIT_URL as string, -// process.env.LIVEKIT_API_KEY as string, -// process.env.LIVEKIT_API_SECRET as string, -// ) -// } + private async initializeRoomServiceClient() { + const { RoomServiceClient } = await import('livekit-server-sdk') + this.roomServiceClient = new RoomServiceClient( + process.env.LIVEKIT_URL as string, + process.env.LIVEKIT_API_KEY as string, + process.env.LIVEKIT_API_SECRET as string, + ) + } -// async createServiceMeetingRoom(chattingRoomId: string) { -// const room = await this.roomServiceClient.createRoom({ -// maxParticipants: 3, -// name: chattingRoomId, -// }) + async createServiceMeetingRoom(chattingRoomId: string) { + const room = await this.roomServiceClient.createRoom({ + maxParticipants: 3, + name: chattingRoomId, + }) -// return room -// } + return room + } -// async createWorkshopMeetingRoom(workshopId: string, maxParticipants: number) { -// const room = await this.roomServiceClient.createRoom({ -// maxParticipants: maxParticipants, -// name: workshopId, -// }) -// return room -// } -// } + async createWorkshopMeetingRoom(workshopId: string, maxParticipants: number) { + const room = await this.roomServiceClient.createRoom({ + maxParticipants: maxParticipants, + name: workshopId, + }) + return room + } +} diff --git a/src/LiveKit/livekit.service.ts b/src/LiveKit/livekit.service.ts index a2ec311..81108fd 100644 --- a/src/LiveKit/livekit.service.ts +++ b/src/LiveKit/livekit.service.ts @@ -1,11 +1,15 @@ -// import { Injectable, OnModuleInit } from '@nestjs/common' -// import { LiveKitRoomService } from './livekit.room.service' +import { Injectable } from '@nestjs/common' +import { LiveKitRoomService } from './livekit.room.service' +import { LiveKitParticipantService } from './livekit.participant.service' -// @Injectable() -// export class LiveKitService implements OnModuleInit { -// private liveKitRoomService: LiveKitRoomService -// async onModuleInit() { -// // init livekit room service -// this.liveKitRoomService = new LiveKitRoomService() -// } -// } +@Injectable() +export class LiveKitService { + constructor( + private liveKitRoomService: LiveKitRoomService, + private liveKitParticipantService: LiveKitParticipantService, + ) {} + + async createAccessToken(participantId: string) { + return await this.liveKitParticipantService.createAccessToken(participantId) + } +} diff --git a/src/MeetingRoom/meetingroom.module.ts b/src/MeetingRoom/meetingroom.module.ts index 7273ef6..c7b12cd 100644 --- a/src/MeetingRoom/meetingroom.module.ts +++ b/src/MeetingRoom/meetingroom.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common' import { MeetingRoomSchema } from './meetingroom.schema' +import { LiveKitModule } from 'src/LiveKit/livekit.module' @Module({ + imports: [LiveKitModule], providers: [MeetingRoomSchema], exports: [MeetingRoomSchema], }) diff --git a/src/MeetingRoom/meetingroom.schema.ts b/src/MeetingRoom/meetingroom.schema.ts index 10543ca..f227f47 100644 --- a/src/MeetingRoom/meetingroom.schema.ts +++ b/src/MeetingRoom/meetingroom.schema.ts @@ -1,13 +1,7 @@ import { Inject, Injectable } from '@nestjs/common' -import { - Pothos, - PothosRef, - PothosSchema, - SchemaBuilderToken, -} from '@smatch-corp/nestjs-pothos' +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' - @Injectable() export class MeetingRoomSchema extends PothosSchema { constructor( @@ -56,14 +50,12 @@ export class MeetingRoomSchema extends PothosSchema { }, resolve: async (_query, _parent, args, ctx: SchemaContext) => { if (ctx.isSubscription) throw new Error('Not allowed') - const collaborationSession = - await this.prisma.collaborationSession.findUnique({ - where: { - scheduleDateId: args.scheduleDateId, - }, - }) - if (!collaborationSession) - throw new Error('Collaboration session not found') + const collaborationSession = await this.prisma.collaborationSession.findUnique({ + where: { + scheduleDateId: args.scheduleDateId, + }, + }) + if (!collaborationSession) throw new Error('Collaboration session not found') const meetingRoom = await this.prisma.meetingRoom.findUnique({ where: { collaborationSessionId: collaborationSession.id, @@ -83,7 +75,12 @@ export class MeetingRoomSchema extends PothosSchema { type: this.meetingRoom(), args: { input: t.arg({ - type: this.builder.generator.getCreateInput('MeetingRoom'), + type: this.builder.generator.getCreateInput('MeetingRoom', [ + 'id', + 'createdAt', + 'updatedAt', + 'collaborators', + ]), required: true, }), },