feat: integrate LiveKit services into Collaboration and Meeting Room modules

- Added LiveKitModule to CollaborationSession and MeetingRoom modules for enhanced real-time collaboration features.
- Updated CollaborationSessionSchema to include LiveKit services for managing participant access and room permissions.
- Implemented a new cron job in CronService to disable services without schedules for over 30 days, improving service management.
- Enhanced GraphQL schema generation with improved filtering logic for better performance and readability.
- Refactored LiveKit services to streamline access token creation and room management functionalities.
This commit is contained in:
2024-12-01 19:18:20 +07:00
parent 111acacf2d
commit 561823225d
10 changed files with 206 additions and 96 deletions

View File

@@ -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 {}

View File

@@ -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) => ({

View File

@@ -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')
}
}
}

View File

@@ -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<Types extends SchemaTypes> {
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<Types>) : this.getFilter(this.mapScalarType(field.type) as InputType<Types>)
type = field.isList
? this.getScalarListFilter(this.mapScalarType(field.type) as InputType<Types>)
: this.getFilter(this.mapScalarType(field.type) as InputType<Types>)
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<Types extends SchemaTypes> {
const fields: Record<string, InputType<Types>> = {}
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<Types extends SchemaTypes> {
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<Types extends SchemaTypes> {
})
}
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,
) {
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<Types extends SchemaTypes> {
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<Types extends SchemaTypes> {
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<Types extends SchemaTypes> {
}) as InputObjectRef<Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Update']>
})
}
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,
) {
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<Types extends SchemaTypes> {
}
}
private getRef<T extends InputObjectRef<Types, unknown>>(key: InputType<Types> | string, name: string, create: () => T): T {
private getRef<T extends InputObjectRef<Types, unknown>>(
key: InputType<Types> | string,
name: string,
create: () => T,
): T {
if (!this.refCache.has(key)) {
this.refCache.set(key, new Map())
}

View File

@@ -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 {}

View File

@@ -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,
{
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) {

View File

@@ -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: <explanation>
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
}
}

View File

@@ -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)
}
}

View File

@@ -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],
})

View File

@@ -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({
const collaborationSession = await this.prisma.collaborationSession.findUnique({
where: {
scheduleDateId: args.scheduleDateId,
},
})
if (!collaborationSession)
throw new Error('Collaboration session not found')
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,
}),
},